Wikilivres
frwikibooks
https://fr.wikibooks.org/wiki/Accueil
MediaWiki 1.47.0-wmf.21
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
Technologie
0
10494
773234
761380
2026-09-26T11:35:08Z
Kontributor 2K
115912
([[c:GR|GR]]) [[c:COM:FR|File renamed]]: [[File:Шаровый кран.jpg]] → [[File:Шаровой кран.jpg]] [[c:COM:FR#FR3|Criterion 3]] (obvious error) · Написание согласно ГОСТ 24856-2014 «Арматура трубопроводная. Термины и определения» и прочим нормам русского языка, сейчас файл используется только в болгарской Вики, там было бы "сферичен кран", так что исходно явно имелся в виду русский язык
773234
wikitext
text/x-wiki
{| border="4" cellpadding="15" cellspacing="0" width=100%
|-
| style="text-align:center; background-color:#77bbff40;" |
<span style="font-size:24pt;">'''TECHNOLOGIE'''</span>
<gallery style="margin:auto;">
5783metric-micrometer.jpg|Micromètre
Pedalarm Bruch.jpg|Rupture par fatigue
Dampfturbine Laeufer01.jpg|Turbine à vapeur
JR-Maglev-MLX01-2.jpg|Train Maglev au Japon
</gallery>
|}
Cette page est l'un des points d'entrée qui permettent d'accéder à la collection des livres de technologie disponibles sur ce site. Pour l'instant, beaucoup de ces livres sont à l'état d'ébauche mais certains sont aujourd'hui très avancés et directement utilisables.
On se retrousse les manches ?
{| cellspacing="0" cellpadding="5" border="1"
|-
| colspan="2"|{{En travaux}}
Ce livre est en complète réorganisation, merci pour votre indulgence !
|-
| style="background-color:#ffff8f40; width:50%;" valign="top"|
<br />
<center><big><u>'''Table des matières (en cours de rédaction)'''</u></big></center>
{{Cotation}}
----
<br />
<br />
{{Plan Éléments théoriques et pratiques}}
{{Plan Matériaux}}
{{Plan Énergie}}
{{Plan Fonctions mécaniques}}
{{Plan Transmissions mécaniques}}
{{Plan Systèmes de manutention}}
{{Plan Dispositifs de fixation et d'assemblage mécanique}}
{{Transports en communs}}
| style="background-color:#abd7ff40; width;50%;" valign="top"|
<br />
{{Technologie}}
|-
|}
----
[[Image:L-Zeichnen.png|thumb|230px|Enseigner la technologie, ce n'est pas seulement faire un « arrêt sur image » dans un film qui se déroule depuis plusieurs millénaires, c'est tenter de montrer le déroulement de ce film et de deviner les séquences qui ne sont pas encore écrites.]]
= Généralités =
# Objet de l'ouvrage {{00}}
# Conventions et vocabulaire {{00}}
# Nomenclature technique {{00}}
# Présentation technique {{00}}
# Règles de la présentation technique {{00}}
= Éléments communs aux divers domaines technologiques =
== Représentation graphique ==
# Méthodes générales du dessin technique {{00}}
# Géométrie descriptive {{00}}
# Dessin de définition {{00}}
# Coupes et sections {{00}}
# [[Perspective]] {{00}}
# Schématisation {{00}}
# [[Dessin assisté par ordinateur]] {{25}}
== Éléments théoriques et pratiques ==
{{Plan Éléments théoriques et pratiques}}
== Matériaux ==
{{Plan Matériaux}}
# Propriétés mécaniques des matériaux {{00}}
== Dimensions, cotation, métrologie ==
# Tolérances {{00}}
# Défauts de surface {{00}}
# [[Tribologie/Nature et comportement des surfaces frottantes|Nature et comportement des surfaces frottantes]] {{75}}
# Cotation fonctionnelle {{00}}
# [[Métrologie]] {{00}}
== Caractéristiques générales des machines ==
# Conception générale et configurations typiques
# Généralités sur les machines à mouvements alternatifs
# Généralités sur les machines à mouvements rotatifs
# Caractéristiques de dimension, de poids, de précision, etc.
== Éléments fixes et mobiles des machines, pièces composantes ==
# Bâtis, fondations, supports, enceintes, etc.
# Parties fixes associées à des éléments mobiles, amovibles, interchangeables, etc.
# Porte-outils, porte-pièces, systèmes d'alimentation et d'évacuation
# Éléments de transmission de la force mécanique, fixations, etc.
# Organes à mouvements alternatifs
# Organes rotatifs en général
# Organes oscillants rigides
# Éléments élastiques, [[Ressort|ressorts]] {{75}}
== Organes de commande et de régulation ==
# Systèmes de contrôle et de régulation des fluides
# Systèmes mécaniques de commande
# Systèmes électriques de commande
== État, condition, forme des matériaux, des surfaces et des produits ==
# États physiques des matériaux gazeux, liquides, solides, granuleux, spongieux, etc.
# États et finitions de la surface des objets
# Objets de surface plane, blocs, lames, plaques, bandes, etc.
# Objets et produits profilés pleins
# Objets et produits profilés creux
# Objets annulaires, perforés, allégés
# Objets et matériaux fibreux
# Matériaux fragmentés, pulvérulents, granuleux, etc.
== Fonctionnement, conduite et contrôle des machines et des processus ==
# Différents types de réglage et de contrôle
# Commande et conduite manuelles
# Commande et conduite automatique des machines et des processus
# Dispositifs de réglage de position
# Régulateurs
# Démarrage, embrayage, couplage
# Organes et dispositifs de contrôle du mouvement, de l'accélération, du sens de marche, etc.
# Organes de retenue, freins, systèmes d'arrêt automatique de sécurité, etc.
== Alimentation et chauffage des machines, carburants, combustibles ==
# Combustibles gazeux
# Combustibles liquides ou utilisés à l'état liquide
# Combustibles solides et pulvérulents
== Service, protection et maintenance des machines, fonctions mécaniques ==
# Dispositifs de refroidissement
# [[Tribologie/Techniques de lubrification|Techniques de lubrification]] {{50}}
# Systèmes de protection contre les éléments extérieurs, abris, enceintes, etc.
# Isolation thermique, calorifugeage {{00}}
# Isolation vibratoire {{00}}
# [[Étanchéité]] {{50}}
# Dispositifs facilitant le montage, le nettoyage, l'entretien, etc.
# Dispositifs de protection des personnes
# Appareils de mesure, de comptage
== Sources d'énergie des machines ==
# Utilisation de la vapeur
# Utilisation de l'énergie hydraulique
# Utilisation de l'énergie électrique
# Utilisation des moteurs à combustion interne
# Utilisation de l'énergie pneumatique et de l'énergie éolienne
# Utilisation de l'énergie mécanique des ressorts, des volants
# Utilisation de la force musculaire
# Méthodes de liaison des moteurs et des machines
# Couplage des machines {{00}}
= Essais des matériaux et des produits =
<gallery>
File:Vickers-path-2.svg
File:DuctileFailure.jpg
File:Ductile fracture upd.png
File:Compression test.jpg
File:Eprouvette charpy schematique.svg
File:Rupture-traction-alu.jpeg
File:Eprouvette traction cylindrique schema.svg
File:Traction cylindrique.jpg
File:Eprouvette resilience.jpg
File:Essais Pliage.jpg
</gallery>
# Machines d'essais
<gallery>
File:Amsler 100 HFP5100.jpg
File:Inspekt desk 50kN IMGP8563.jpg
File:Mouton charpy.JPG
File:Losenhausenwerk IMGP8565 mit typenschild.jpg
</gallery>
# Échantillonnage
# Tests d'aptitude au service
# Mesure des propriétés mécaniques des matériaux : traction, compression, dureté, endurance, etc.
<gallery>
File:DMS Messrosette.svg
</gallery>
# Essais non destructifs, détection des défauts
# Analyse de la structure des matériaux, métallographie
# Corrosion
# Protection des matériaux
= Économie de l'énergie =
# Stations génératrices (études générales seulement)
# Sources d'énergie
# Récupération de l'énergie
# Énergie nucléaire
= Mécanique industrielle, électrotechnique =
== Technologie nucléaire ==
# Matières premières
# Matériaux fissiles élaborés
# Réacteurs nucléaires
# Questions de sécurité
== Machines thermiques, production et utilisation de la vapeur ==
=== Production de la vapeur ===
# Production de la chaleur {{00}}
# Chaudières {{00}}
<gallery>
File:Tetlow High Pressure boiler TM153.png
</gallery>
# Systèmes auxiliaires des chaudières {{00}}
# Traitement des eaux destinées aux chaudières {{00}}
=== Utilisation de la vapeur ===
# [[Machines à vapeur alternatives]] {{00}}
<gallery>
File:Georgetown PowerPlant Museum 108.jpg
</gallery>
# Turbines à vapeur {{00}}
# Condensation de la vapeur {{00}}
== Énergie hydraulique ==
=== Roues et turbines hydrauliques ===
<gallery>
File:Francis pump-turbine for Shintoyone power station.jpg
File:Francis turbine for Sakuma power station.jpg
File:Francis turbine for Misakubo power station.jpg
File:Fankel Generator Kupplung Turbine 02.jpg
File:Fankel Generator Kupplung Turbine 01.jpg
File:Fankel Francisturbine 02.jpg
File:Fankel Francisturbine 01.jpg
File:Ossberger turbine runner.jpg
File:Ossberger turbine.jpg
</gallery>
=== Béliers hydrauliques ===
== Électrotechnique ==
[[Électrotechnique/Images en réserve|Images en réserve]]
=== Études générales ===
# Théorie, définitions, concepts
=== Production, approvisionnement de l'électricité, machines électriques, appareils de mesure ===
# Usines génératrices
<gallery>
File:ManitobaHydroPointeduBois Generator.jpg
</gallery>
# Machines électriques, générateurs et moteurs
<gallery>
File:StepperMotor.gif
File:Wimshurst machine, without segments (Rankin Kennedy, Electrical Installations, Vol V, 1903).jpg
File:Wimshurst machine (Rankin Kennedy, Electrical Installations, Vol V, 1903).jpg
File:Mouse mill motor, rotor and coils (Rankin Kennedy, Electrical Installations, Vol V, 1903).jpg
File:Moteur brushless.png
File:Stepper motor.svg
File:Höllentalbahn (Niederösterreich) Betriebsgebäude in Reichenau innen1.jpg
File:Fankel Generator 01.jpg
File:Fankel Generator Kupplung Turbine 02.jpg
File:Fankel Generator Seilkupplung 01.jpg
File:Uzwojenie.jpg
File:Moteur à cage.JPG
File:CEM_BBA_numéro_46462.jpg
File:Ancien moteur à courant alternatif bobiné en anneau au stator.jpg
File:Alitoh numéro 19021.jpg
File:Alioth numéro 15720.jpg
File:Alioth 1897.jpg
File:Moteur_Alioth_et_Cie_vers_1891.jpg
File:Intérieur Alioth biphasé sans rotor 1893 .jpg
File:Rotor_d'un_moteur_à_induction,_diphasé,_fabriqué_par_R._Alioth_et_Cie,_numéro_1042,_daté_vers_1891..jpg
File:Moteur_à_induction,_diphasé,_fabriqué_par_R._Alioth_et_Cie,_numéro_1042,_daté_vers_1891.jpg
File:Moteur Legendre frères à coupleur.JPG
</gallery>
# Transformation, transformateurs, convertisseurs, etc.
# Transmission de l'énergie électrique, câbles, lignes, réseaux, isolants
# Distribution et régulation de l'énergie électrique, interrupteurs, disjoncteurs, matériel de protection, etc.
<gallery>
File:Single pole quick-break switch, wooden base (Forty Years of Electrical Progress).jpg</gallery>
# Méthodes et appareillages de mesures électriques et magnétiques
<gallery>
File:Precisionsampèremeter, det inre, Nordisk familjebok.png
File:Precisionsampèremeter, det yttre, Nordisk familjebok.png
File:Voltmètre R.Alioth&Cie.jpg
</gallery>
# Magnétisme appliqué, bobines, relais
# Électrostatique appliquée
=== Sources de lumière électrique ===
# Lampes à arc
# Lampes à incandescence
# Lampes et tubes à décharge
# Lampes combinées
# Diodes électroluminescentes
=== Traction électrique ===
# Électrification des chemins de fer
# Lignes d'alimentation électrique des véhicules
# Moteurs électriques de traction
# Systèmes de captage du courant
# Organes de commande et de contrôle des véhicules électriques
=== Technologie électrochimique ===
# Piles et batteries
# Électrolyse, électrodéposition, anodisation
=== Thermoélectricité ===
# Fours à arc
# Chauffage direct par résistance électrique
# Chauffage indirect par fluide caloporteur
# Chauffage par induction
# Chauffage électrique combiné à d'autres sources de chaleur
=== Technique des ondes électriques et électromagnétiques ===
# Propagation non guidée des ondes électromagnétiques
# Propagation guidée des ondes et oscillations électriques
# Générateurs d'oscillations et d'impulsions
# Multiplicateurs et diviseurs de fréquences
# Amplificateurs
# Modulation et démodulation
# Dispositifs de retard et de mémoire
=== Dispositifs électroniques, tubes électroniques, accélérateurs de particules, tubes à rayons X ===
# Dispositifs à semi-conducteurs, transistors
# Tubes et cellules photoélectriques
# Production et application des rayonnements corpusculaires
# Tubes à électrons, tubes thermo-ioniques
# Appareils à rayons X
# Tubes à décharge
# Systèmes opto-électroniques, diodes électroluminescentes
# Lasers
=== Télécommunications, vidéo, télécontrôle, etc. ===
# Notions générales, théorie de l'information, théorie des signaux
# Télégraphie
# Téléphonie
# Appareils de radiocommunication
# Émetteurs et antennes
# Appareils et méthodes spéciaux, radar
# Technique vidéo, télévision
# Télécontrôle, télémesure, télécommande
== Moteurs thermiques ==
=== Moteurs à air chaud ===
# Théorie
# Moteurs Stirling
# Équipements auxiliaires
=== Moteurs à combustion interne ===
# Théorie, [[Les différents cycles|cycles]] {{00}}
# [[Moteurs thermiques/Histoire|Histoire]] {{00}}
# Systèmes de vaporisation, de carburation, d'injection
<gallery>
File:Carburetor types typy gaźników.svg
</gallery>
# Systèmes d'allumage
# Études de la combustion
# Dispositifs d'échappement
<gallery>
File:Desmodromiczne sterowanie zaworów.svg
</gallery>
# Constitution des moteurs alternatifs {{00}}
<gallery>
File:Variable stroke engine (Autocar Handbook, Ninth edition).jpg
File:Cylinder block and head of sidevalve engine (Autocar Handbook, Ninth edition).jpg
</gallery>
# Constitution des moteurs rotatifs
<gallery>
File:Wankel Cycle anim tr.gif
</gallery>
# Problèmes dynamiques, équilibrage {{00}}
# Turbines à gaz {{00}}
<gallery>
File:GaTurbineBlade.svg
</gallery>
# Turboréacteurs
=== Autres types de moteurs thermiques ===
# [[/Moteurs thermiques/Moteur Diesel|Moteur Diesel]]
== Énergie pneumatique, réfrigération ==
=== Compression et déplacement de l'air et des gaz ===
# [[Compresseurs]] {{00}}
# Ventilateurs à haute pression et soufflantes
=== Raréfaction de l'air et des gaz ===
# Pompes à vide
<gallery>
File:Sperrschieberpumpe.svg
File:PompaVacuo.png
File:Fonctionnement trompe a eau.png
</gallery>
# Raréfaction de l'air ou des gaz par des procédés chimiques et physicochimiques
=== Applications de l'air comprimé ou raréfié ===
# Moteurs pneumatiques
# Outils pneumatiques
# Éoliennes, utilisations de l'énergie éolienne
<gallery>
File:Verin rotatif principe.svg
</gallery>
=== Réfrigération en général ===
# Mélanges réfrigérants
# Fluides réfrigérants
# Installations de refroidissement
# Conservation du froid
=== Machines réfrigérantes, pompes à chaleur ===
# Machines à absorption
# Machines à compression
# Dispositifs à effet Peltier
# Pompes à chaleur
=== Production de glace ===
== Distribution, stockage et manipulation des fluides ==
=== Généralités ===
# Types de fluides à manipuler
# Méthodes et installations
=== Souffleries et machines de souffleries ===
<gallery>
File:Ejector or Injector.svg
</gallery>
=== Ventilateurs, extracteurs d'air ===
=== Installations de stockage et de distribution, équipements sous pression ===
# Récipients, conteneurs, réservoirs
# Appareils à pression {{00}}
# Tuyauteries {{00}}
<gallery>
File:Hoseclamps.screw.agr.jpg
</gallery>
# Raccordement des tuyauteries {{00}}
<gallery>
File:Demi raccord symetrique guillemin.svg
File:Demi raccord symetrique guillemin perspective.svg
File:Gama_racores_UNE23400_ligatura.JPG
File:Reducción BcnØ45 a BcnØ25.JPG
File:Kupplung.jpg
</gallery>
=== Obturateurs et robinetterie ===
<gallery>
Шаровой кран.jpg
Valve cross-section.PNG
Дисковый клапан.jpg
Двухстворчатый клапан открыто.jpg
Двухстворчатый клапан 1.jpg
Дисковый клапан1.jpg
Ball-valve-ball-The-Alloy-Valve-Stockist.jpg
Butterfly-valve--The-Alloy-Valve-Stockist.JPG
Polte Hochdruckgasschieber1.JPG
Polte be-und Entlüftungsventil1.JPG
Polte Drosselklappe für Turbinen.JPG
Polte Rückschlagklappe gross.JPG
Polte Ueberdruckventil.JPG
Polte Wasserabsperrschieber.JPG
Polte Kesselarmaturen.JPG
Polte Gruppe von Schiebern.JPG
Vannes-boisseau.JPG
</gallery>
# Dispositifs de pulvérisation
# Mécanismes d'élévation de l'eau
# [[Pompes]] {{00}}
=== Circuits hydrauliques et pneumatiques ===
# Composants des circuits hydrauliques {{00}}
<gallery>
File:Pompe pistons axiaux camion en position.JPG
File:Pompe pistons axiaux camion plateau barillet axe.JPG
File:Pompe pistons axiaux camion barillet vue dessous.JPG
</gallery>
# Composants des circuits pneumatiques {{00}}
# Conception des circuits {{00}}
<gallery></gallery>
== Formage et usinage sans enlèvement de matière ==
=== Principes et théorie du façonnage ===
# Procédés
# Machines
# Outillages
# Modes de travail (à chaud, à sec, etc.)
# Préparation des pièces
# Finition des pièces
# Contrôle des pièces
=== Usinage par déformation plastique ===
# Méthodes de forgeage
<gallery>
File:Machine numérique.jpg
File:Two machined forged pieces (1).jpg
File:Two machined forged pieces (2).jpg
File:Two machined forged pieces (3).jpg
File:Two machined forged pieces (4).jpg
File:Filage sur aiguille.png
File:Filage inverse.png
File:Filage direct.png
File:Filage contenu.png
File:ForgedPiece.jpg
File:Electromagnetic Forming 01.png
File:Forging 3.jpg
File:James Nasmyth's patent steam hammer.jpg
File:Zgniatanie.jpg|Fluotournage
File:Roll forging 3D.gif
</gallery>
# Outillages de forgeage
# Matriçage
# Déformation par explosion
# Déformation par magnétostriction
=== Fonderie, moulage ===
# Méthodes de moulage
<gallery>
Animatie hydraulisch kniehefboom sluitsysteem.gif
Cold chamber die casting machine schematic 5.svg
SandMoldBronzeAluminium.jpg
SandMoldCopeDragCores.jpg
Low pressure casting model N.PNG
Casting mould for pump impellor.JPG
Foundry defect blowhole.jpg
Procédé V-process.JPG
Phases de moulage.JPG
Plaques modèle.JPG
Noyau Croning.JPG
Ondersnijding.gif
Ondersnijding.jpg
Casting riser types blank.svg
Moule masselotté.JPG
Touret-centrifugation.JPG
</gallery>
# Préparation des moules et des noyaux
# Préparation des modèles
# Machines de moulage
# Appareils de fusion
<gallery>
Cubilot-principe.JPG
</gallery>
# Systèmes de coulée
# Finition des pièces moulées
# Moulage des matières plastiques
<gallery>
Plastic Injection Moulding Clamping Phase.gif
Plastic Injection Moulding Ejection Phase.gif
</gallery>
=== Marquage, assemblage, contrôle, ajustage des pièces ===
<gallery>
File:Laserengravingonsteel.jpg
</gallery>
# Tolérances de fabrication
# Jauges et calibre
# Ajustage
# Techniques d'assemblage et de montage
=== Métallurgie et travail des poudres et des fibres ===
# Fabrication des poudres
# Frittage {{00}}
# Imprégnation
# Fabrication des matériaux composites à fibres de renfort
=== Laminage, extrudage, chaudronnerie ===
# Laminage, laminoirs
<gallery>
File:Zumaia Ancien Laminoir.jpg
</gallery>
# Chaudronnerie, fabrication de récipients à pression
# Fabrication par déformation des tubes et des tuyaux
# Extrusion
# Étirage, travail des barres et des fils
=== Traitements thermiques et autres ===
# Méthodes de chauffage
# Fours, bains et autres dispositifs de chauffage
# Bains de refroidissement
# Procédés de traitement, trempe, recuit, revenu, cémentation
# Traitements mécaniques par pression, par frappe, par étirement, etc.
=== Soudage, collage, traitements superficiels, finition ===
# [[Soudage]]
# [[Brasage]]
# Rechargement
# Oxycoupage et autres méthodes de coupage à chaud
# [[Collage]]
# Revêtements de surface, métallisation
# [[Traitements de surface]] chimiques et électrochimiques
=== Magasinage, entreposage, stockage ===
=== Maintenance et réparation ===
=== Emballage et conditionnement des produits ===
== Éléments de machines, manutention, levage ==
=== Études générales ===
=== Arbres, tourillons, paliers, accouplements ===
# Arbres de transmission {{00}}
# [[Tribologie/Guidage par glissement de surfaces|Guidage par glissement de surfaces]] {{25}}
# [[Tribologie/Guidage par roulement|Guidage par roulement]] {{25}}
# [[Tribologie/Guidage par sustentation magnétique|Guidage par sustentation magnétique]] {{25}}
# Accouplements d'arbres {{00}}
<gallery>
Цепная муфта.png
Twin-plate Link Coupling - NMST - IIT Kharagpur - West Midnapore 2015-09-28 4598.JPG
Parallel Link Coupling - NMST - IIT Kharagpur - West Midnapore 2015-09-28 4596.JPG
File:Ausgleichskupplung.JPG
File:Tárcsás tengelykapcsoló.svg
File:Tokos tengelykapcsoló.svg
File:Helical U-Joint (Beam Style Coupling) - Angular Misalignment.jpg
File:Special Beam Couplings with attachments.jpg
File:Beam Coupling - Helical Standard.jpg
File:Sprzeglo oponowe-schemat.png
File:Plate clutch-diagram.png
File:Vernier coupling (Manual of Driving and Maintenance).jpg
File:Gear coupling.png
File:Fankel Generator Seilkupplung 01.jpg
File:Fankel Seilkupplung 01.jpg
File:Kupplung elast..JPG
</gallery>
# [[Joints de transmission]] {{00}}
# Articulations {{00}}
# Bielles, manivelles et pistons {{00}}
# Excentriques {{00}}
# Systèmes articulés, coulisses {{00}}
=== Transmissions mécaniques ===
{{Plan Transmissions mécaniques}}
# Réducteurs et multiplicateurs de vitesse {{00}}
<gallery>
File:Cycloidal drive.gif
</gallery>
# Boîtes de vitesses {{00}}
<gallery>
</gallery>
# Cames, croix de Malte, systèmes d'indexation {{00}}
<gallery>
File:Mécanisme du cinématographe des frères Lumière.gif
File:Luch2 greifer.gif
File:Geneva mechanism 6spoke animation.gif
File:Cams.svg
File:Came disque types suiveurs.svg
</gallery>
# Glissières
<gallery></gallery>
# [[Embrayages]] {{00}}
# limiteurs de couple
<gallery>
File:Torque limiter with balls Make Ringspann.JPG
File:Spring torque limiter make Ringspann.JPG
</gallery>
# Changements de marche {{00}}
# [[Freins]] {{00}}
<gallery></gallery>
# [[Transmissions par friction]] {{00}}
# Transmissions par câbles et autres liens souples {{00}}
==== Chaînes et roues dentées ====
<gallery>
File:Chain.gif
File:Renold's silent chain.jpg
File:Renold's silent chain, chain and wheel engagement.jpg
File:Britannica Bicycle Chain-Wheel Lines.jpg
File:Przekladnia lancuchowa 2.svg
</gallery>
# [[Système vis-écrou]] {{00}}
# Vis de transformation de mouvement {{00}}
# Variateurs de vitesse {{00}}
<gallery>
File:GearBoxRotRotVar.gif
File:Variator A.png
File:Variator B.png
File:Variator C.PNG
File:Variator D.png
File:Variator E.png
File:Variator CB2.png
</gallery>
# Systèmes de retour rapide {{00}}
=== Manipulation des matériaux et des produits, manutention (621.86) ===
{{Plan Systèmes de manutention}}
# Généralités sur les méthodes et le choix des appareils {{00}}
# Dispositifs de dosage
<gallery>
File:Pompe liquides.jpg
</gallery>
# Dispositifs pour la manutention manuelle
# Crics, vérins {{00}}
# Manipulateurs et robots
# Élévateurs, transporteurs, convoyeurs {{00}}
<gallery>
File:CentrelessConveyor.JPG
File:Convoyeur à bande.JPG
File:Convoyeur a rouleaux coniques.jpg
File:Bechergurt BMK.jpg
File:Erster Hubtisch1.jpg
File:Anlagehebebühne.jpg
File:Support élévateur.JPG
</gallery>
# Équipements accessoires, palettes, conteneurs de transport, outils à main
<gallery>
File:Side By Side P.png
File:Yhst-33833170891817 1977 3727729.jpg
</gallery>
=== Grues et autres systèmes de levage ===
{{Plan Systèmes de levage}}
# Organes directement liés au déplacement de la charge {{00}}
<gallery>
File:Stainless steel shackle.jpg
File:Nasukan.jpg
File:Thimble for the stay of radio antennae.jpg
File:Handy-winch.jpg
</gallery>
# Organes de sécurité {{00}}
# Grues {{00}}
# Ponts roulants {{00}}
# Ascenseurs, monte-charges, escalators {{00}}
=== Dispositifs de fixation et d'assemblage mécaniques ===
{{Plan Dispositifs de fixation et d'assemblage mécanique|Dispositifs de fixation et d'assemblage mécanique}}
[[Anneaux élastiques]]
# Classification des assemblages et liaisons mécaniques {{00}}
<gallery>
File:Keilprofil-6K-Detail.JPG
File:Keilprofil-6K.JPG
File:Zugkeilverbindung.svg
</gallery>
# [[Assemblages directs]], sans organe de liaison, sertissage, [[dudgeonnage]]
# Outils de fixation, de serrage, de blocage, étaus, tenailles, pinces, etc.
# Généralités sur les filetages {{00}}
# [[Boulonnerie, visserie et accessoires]] {{00}}
<gallery>
File:Cross dowel cutaway.png
File:Cross dowel.png
</gallery>
# Dispositifs de manœuvre des pièces filetées {{00}}
# Dispositifs contre le desserrage des pièces filetées {{00}}
==== Rivetage et procédés assimilés ====
<gallery>
Popnagel.gif
Rivet01.jpg
Explanation graphic.jpg
Nit rurkowy.jpg
Round Head Rivet.JPG
Direct riveting.png
Rivets02.jpg
</gallery>
=== Éléments d'assemblage divers, fermoirs, manchons, bagues, etc. ===
{{00}}
<gallery>
DIN 1475.png
DIN 1474.png
DIN 1473.png
DIN 1472.png
DIN 1471.png
File:Cotter Pin (PSF).png
File:Snodo tubi inn.jpg
File:Cotter-Pin.jpg
File:Polaczenie klinowe-schemat.png
File:Futurfix.gif
File:Combifix.gif
File:Isofonica.gif
File:Hairpin clip.png
File:Lynch-pin.jpg
File:R-Clip.jpg
File:Krallenkeil für Beil bzw Axt 1747.JPG
File:Universaldübel 8x50 und Rundhaken 1755.JPG
File:Metall-Fensterrahmendübel F10 M132 1756.JPG
File:Rundkopfstift, vermessingt, 1,2x20 mm 1733.JPG
File:Parallel pin.svg
File:Grooved pin with chamfer.svg
File:Grooved pin with round head.svg
File:Spring pin light duty.svg
File:Taper pin.svg
File:Nagel 1649.JPG
File:6er Spreizdübel1647.JPG
File:Metall-Splint.jpg
</gallery>
==== Assemblage rigide d'une tige sur un moyeu {{00}} ====
<gallery>
File:GM 6.2 diesel damaged cam key 2.jpg
File:GM 6.2 diesel cam key removed 2.jpg
File:Parallel keys.png
</gallery>
=== Frottement, lubrification, lubrifiants, dispositifs de lubrification ===
# [[Tribologie]] {{75}}
# [[Tribologie/Lubrifiants|Lubrifiants]] {{100}}
# [[Tribologie/Techniques de lubrification|Techniques de lubrification]] {{50}}
== Usinage par enlèvement de matière, travail des métaux en feuilles ==
=== Notions théoriques et générales ===
# Modes de travail des matériaux
# Outils coupants
# Théorie de la coupe des métaux
# Appareils et équipements auxiliaires
<gallery>
File:Kraftspannspindeln in Planscheibe.jpg
</gallery>
=== Procédés particuliers ===
# Usinage par explosifs
# Usinage électrochimique
# Usinage par électro-érosion
<gallery>
File:Wire erosion.png
</gallery>
# Usinage par ultrasons
<gallery>
File:Ultrasonic machining schematic.jpg
</gallery>
=== Usinage par coupe ===
<gallery>
File:Animiertes Prinzip mechan-Hobelmaschine-3D.gif
File:Zahlubnik.jpg
File:Кінцева фреза.JPG
File:Дискові фрези.JPG
File:Back-knife lathe (Carpentry and Joinery, 1925).jpg
File:Wood turning lathe (Carpentry and Joinery, 1925).jpg
File:Planing and thicknessing machine (Carpentry and Joinery, 1925).jpg
File:Horizontal log band mill (Carpentry and Joinery, 1925).jpg
File:Spiral turning lathe (Carpentry and Joinery, 1925).jpg
</gallery>
# Rabotage
<gallery>
File:1-1111 ShaperSlideClapperBoxNumbered.jpg
</gallery>
# Mortaisage
==== Fraisage ====
<gallery>
File:Sharp 3 Axis Vertical Mill Turret Track Detail.jpg
File:Sharp 3 Axis Vertical Mill Turret Rotation Detail.jpg
File:Sharp 3 Axis Vertical Mill Full View.jpg
File:MillingCutterSlotEndMillBallnose.jpg
File:MillingCutterHSSSlab.jpg
File:MillingCutterCarbideTippedFaceMill-INT40.jpg
File:MillingCutterHobbingInvolute.jpg
File:Horizontal milling machine--Cincinnati--early 1900s--001.png
File:Threading tap.JPG
File:Fraisage en concordance.svg
File:Fraisage en opposition.svg
File:AlCrTiN-CoatedHob NanoShieldPVD Thailand.jpg
File:Carbide tipped face mill (1).jpg
File:Carbide tipped face mill (2).jpg
File:Carbide tipped face mill (3).jpg
File:Carbide tipped face mill (4).jpg
File:Milling cutters (1).jpg
File:Milling cutters (2).jpg
File:Cutting inserts (1).jpg
File:Cutting inserts (2).jpg
File:Cutting inserts (3).jpg
File:Cutting inserts (4).jpg
File:Spline hobs.JPG
File:Milling splineprofile.JPG
File:Hobbing machine.jpg
File:Orbital-drilling.gif
File:Торцева фреза.JPG
File:Торцова фреза.JPG
File:Fraisage trochoidal.jpg
</gallery>
# Travail manuel, limage, râpage, grattage
==== Brochage ====
<gallery>
File:BroachPushChipBreakers.jpg
File:Modular broach.jpg
File:Broach types and examples.jpg
File:Zahnrad mit Keilnabe.JPG
File:Geräumte-Kerbverzahnung2.JPG
</gallery>
# Perçage
<gallery>
File:Flowdrill.png|fluoperçage
</gallery>
==== Tournage ====
<gallery>
LatheCarbideTippedTool.jpg
LatheCarbideTippedBoringThreadingBars.jpg
FourJawChuckIndependent.jpg
ThreeJawChuckKey.jpg
CenterLiveDead.jpg
Usinage interne.jpg
Usinage externe.jpg
Navrtávák.jpg
</gallery>
# Alésage
# Usinage par abrasion, affûtage, meulage, polissage, rectification
<gallery>
Centerless grinding schematic.svg
Centerless grinder wheels.jpg
Centerless grinder.jpg
Schleifstein Nettersheim 1681.jpg
</gallery>
# Sciage, découpe, débit, tranchage
<gallery>
Water jet cutter head.svg
Waterjet-cutting-close-up.jpg
Puntzonatutako xafla.jpg
Puntzonaketa-prozesua.jpg
Eskuzko puntzonagailua.jpg
</gallery>
=== Procédés divers ===
# Broyage et concassage
# triage, criblage, tamisage, séparation
# Malaxage, mélange
=== Outillages et machines ===
# Cisailles
# Marteaux
# Presses
=== Travail des métaux en feuilles ===
# Pliage, cintrage, bordage
<gallery>
File:Swing-folding-ani.gif
</gallery>
# Redressage, planage
# Emboutissage
# Repoussage
=== Filetage et taraudage ===
<gallery>
File:Filet électrique.jpg
File:Planetary thread rolling.svg
File:Thread rolling 2 die.svg
File:Thread rolling 3 die.svg
File:Vratidlo.jpg
File:Flowdrill.png
File:Screw (bolt) 07A-J.PNG
File:Screw (bolt) 07B-J.PNG
File:Screw (bolt) 07C-J.PNG
File:Screw (bolt) 13-n.PNG
File:Screw (bolt) 14-n.PNG
File:Screw (bolt) 15-n.PNG
File:Screw (bolt) 17-n.PNG
File:Screw (bolt) 18-n.PNG
</gallery>
== Industrie minière ==
=== Généralités ===
=== Exploration des gisements ===
=== Exploitation des gisements ===
=== Exploitation de roches spécifiques ===
=== Ingénierie du conditionnement d'air dans les mines ===
=== Épuisement des eaux ===
=== Extraction et manipulation des produits extraits ===
<gallery>
File:Roue à chevilles.png
</gallery>
=== Préparation et traitements des minerais ===
=== Questions de sécurité ===
== Génie militaire ==
== Génie civil ==
=== Généralités ===
# Structures et projets
# Organisation et projets de chantiers
=== Infrastructures, fondations, tunnels ===
# Géotechnique, mécanique des sols, techniques de congélation des terrains
# Ouvrages et matériaux pour les climats froids
# [[Ingénierie des fondations]] {{00}}
# Culées et piles
# Construction des tunnels et galeries
=== Ponts ===
<gallery>
</gallery>
# Ponts en treillis
# Ponts à câbles
# Ponts en arc
# Ponts mobiles
=== Superstructures ===
== Technique des transports par voie terrestre ==
=== Généralités ===
# Dimensionnement des revêtements routiers
# Interactions entre les véhicules et les voies ou les routes
# Liants, matériaux de liaison
# réalisation des revêtements
=== Chemins de fer ===
# Les voies ferrées
<gallery>
File:Scharfenbergkupplung Zeichnung.png
File:Balance.svg
File:Balance near derailment.svg
File:Balance near derailment.PNG
File:Syntegra3.jpg
File:Rail.jpg
File:Vías del Ferrocarril Trasandino.jpg
</gallery>
# Chemins de fer particuliers
# Chemins de fer urbains, tramways
# Chemins de fer à câbles, funiculaires, téléphériques
<gallery>
File:Entrainement.jpg
File:Cable lest.jpg
</gallery>
=== Construction routière ===
# Types de routes
# Tracé des routes
# Éléments de la chaussée
# Méthodes de construction des routes
# Entretien et réparation des routes
# Installations environnementales et souterraines
# Types de revêtements routiers
== Ingéniérie hydraulique, construction hydraulique ==
== Génie sanitaire, assainissement, éclairage ==
== Ingéniérie des véhicules de transport ==
= Agriculture, sylviculture, exploitation agricole, élevage, chasse, pêche =
Voir notamment
* [[w:fr:Prosylva|méthode Prosilva]]
* [[Méthode Speich]]
= Économie domestique, science ménagère =
= Gestion et organisation de l'industrie, du commerce et de la communication =
== Généralités ==
== Organisation des bureaux, bureautique ==
== Télécommunications et télécontrôle ==
== Industries graphiques, imprimerie, typographie, édition ==
== Transports et services postaux, organisation et contrôle du trafic (656) ==
=== Généralités (656.0) ===
=== Transport routier (656.1) ===
=== Transport ferroviaire (656.2) ===
=== Transport par chemins de fer spéciaux (656.3) ===
=== Lignes et services de tramways (656.4) ===
{{Modèle:Plan Lignes et services de tramways}}
=== Autres formes de transport terrestre (656.5) ===
=== Transport par voie d'eau (656.6) ===
=== Transport par air, trafic aérien (656.7) ===
=== Services postaux (656.8) ===
=== Types divers d'agences d'expédition, courtage (656.96) ===
== Comptabilité ==
{{En travaux}}
<gallery>
File:Orient Runabout 1907 - 2.JPG
</gallery>
== Construction métallique ==
# [[Construction métallique/Structures métalliques|Structures métalliques]] {{00}}
# [[Construction métallique/Poteaux|Poteaux]] {{00}}
# [[Construction métallique/Poutres|Poutres]] {{00}}
# [[Construction métallique/Planchers|Planchers]] {{00}}
# [[Construction métallique/Escaliers|Escaliers]] {{00}}
# [[Construction métallique/Réalisation des éléments de structure|Réalisation des éléments de structure]] {{25}}
# [[Construction métallique/Assemblage des éléments de structure|Assemblage des éléments de structure]] {{00}}
== Construction en bois ==
== Construction en béton ==
# Constitution des bétons {{00}}
# Principes de calcul {{00}}
# Mise en œuvre des bétons {{00}}
# Poteaux, poutres {{00}}
# Béton précontraint {{00}}
Verre
<gallery>
File:Glass Forming Process Blow-Blow.svg
</gallery>
{{En travaux}}
<gallery></gallery>
'''Conclusion et recommandations générales''' {{00}} <br />
''Annexes'' {{00}} <br />
# [[Guide libre du dessinateur industriel - Torseur de cohésion|Torseur de cohésion]] {{25}}
''Bibliographie'' {{00}}
[[Catégorie:Dessin technique]]
[[Catégorie:Technologie]]
[[Catégorie:Guide libre du dessinateur industriel (livre)|!]]
pn51vkfya3xfvd1m1srofaau7bwp69n
Fonctionnement d'un ordinateur/Les processeurs superscalaires
0
65956
773115
773045
2026-09-25T14:29:18Z
Mewtow
31375
/* L'évolution historique des processeurs superscalaires */
773115
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
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.
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
===L'évolution historique des processeurs superscalaires===
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. La compétition n'était pas en reste, avec le Motorola 88110, l'architectures Alpha 21064, le CPU PA-RISC 7100, MIPS avec le R1000, le SuperSPARC. Tous ont eu des successeurs, qui étaient tous des CPU superscalaires plus ou moins évolués. La timeline précise est décrite dans le tableau ci-dessous.
La plupart des CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 sont rapidement montés à de la quadruple émission, avec quelques exceptions. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 ||
|-
! 1995
| 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000
|-
! 1996
| class="f_bleu | 620 || || || || class="f_bleu | 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
==Les unités de calcul d'un processeur superscalaire==
Étudier les CPU superscalaires historique était une bonne introduction propédeutique, nous allons maintenant passer aux CPU superscalaires en général. Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Et cela nous permettra de généraliser certains concepts vus sur les CPU superscalaires historiques.
Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est possible d'exploiter plusieurs unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu.
Les tout premiers CPU superscalaires étaient nombreux à utiliser cette technique. Ils étaient des CPU double émission, avec une ALU entière, une unité mémoire et une FPU. Nous parlerons de '''partitionnement INT-FLOAT-MEM'''. Il s'agit de l'HyperSPARC, de l'alpha 21064 et du PowerPC 603. Les contrainte d’appariement variaient grandement suivant le processeur. Les plus permissifs autorisaient toutes les combinaisons possibles entre : une opération entière, une opération flottante, et une unité mémoire. Mais ils n'étaient pas les seuls.
Le PowerPC 603 implémentait ce genre de double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions).
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Le '''reconditionnement d'une unité fonctionnelle''' permet à celle-ci de faire des opérations qu'elle n'est pas prévue pour. Par exemple, il est possible d'utiliser l'unité mémoire comme seconde ALU entière, ou d'utiliser la FPU comme multiplieur. Ces deux exemples ne sont pas choisit au hasard, nous les étudierons dans ce qui suit. Le reconditionnement évite d'avoir à dupliquer une unité de calcul, tout en ajoutant une voie. Il permet d'améliorer l'efficacité du partitionnement, qu'il soit utilisé seul ou non.
Il est possible de reconditionner la FPU pour faire des multiplications entières. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits. Il est possible d'utiliser de multiplieur pour faire des multiplications entières 32 bits. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
Le reconditionnement de la FPU permet d'exécuter une multiplication entière en plus des autres opérations entières. Par exemple, prenons un CPU avec double émission entière-flottante, sans multiplieur entier séparé. Le reconditionnement de la FPU permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine.
Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants.
Passons maintenant à une seconde forme de reconditionnement. Reconditionner l'unité mémoire permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, des versions améliorées de l'Intel 960. L'Intel 960 originel n'était pas superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire au monde. Il intégrait une ALU entière, une unité mémoire et une unité de branchement. Grâce au partitionnement, il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, en remplacement d'une µop mémoire. Son pipeline permettait donc les combinaisons INT + BRANCH + (MEM / + INT*).
Le cas de l'i960 nous montre que le reconditionnement permet d'améliorer l'intérêt du partitionnement. Elle permet de ne pas dupliquer d'unité de calcul, tout en permettant quand même d'émettre une seconde opération entière.
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''.
Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières ! Il doit y avoir deux ALU entières pour de la double émission entière, trois pour de triple émission éntière, 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]]
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications.
Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que dupliquer l'ALU entière peut se marier parfaitement avec la présence d'une FPU. Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU reconditionne sa FPU ou pas. Mais il est très rare qu'un CPU ait à la fois un multiplieur entier et une FPU reconditionnée.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
Le processeur SuperSPARC avait deux ALU entière et une FPU. La FPU était reconditionnée, ce qui fait que la multiplication était faite dans la FPU, idem pour la division. Il lui est possible d'émettre deux opérations entières simples, en plus d'une multiplication. Par contre, on perd la possibilité d'émettre en même temps une multiplication et une opération flottante. Les combinaisons INT + INT + (MUL/FLOAT) sont possibles pas les combinaisons INT + MUL + FPU.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires modernes utilisent toutes ces stratégies===
Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, duplication de l'ALU entière, duplication des autre unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents.
* Le partitionnement a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU.
* Le reconditionnement des unités a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU.
* La duplication des ALU entières est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants.
* La duplication des autres unités a un cout en circuit variable, pour des gains en performance eux aussi variables.
De nos jours, le partitionnement est utilisé en complément de la duplication des ALU entières.
L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. Ces modèles utilisaient presque tous la double émission avec trois unités : une ALU, une FPU et une unité mémoire. Si la moitié des processeurs faisait de la double émission "complète", l'autre moitié utilisait une forme encore plus simple de partitionnement.
La '''double émission entière-flottante''' consiste à émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le processeur a donc deux pipelines : un pipeline entier, et un pipeline flottant. Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire, vu que les µops mémoire passent par l'ALU entière avant d’atterrir dans l'unité mémoire. Le reconditionnement de la FPU était utilisé sur les processeurs à double émission entière-flottante. Il entrainait un léger en performance, pour un cout en circuits presque nul.
{|
|[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU.]]
|[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
|}
Les autres processeurs utilisaient uniquement le reconditionnement, mais séparaient l'unité mémoire de l'ALU entière. Ils faisaient donc du ''partitionnement INT-FLOAT-MEM'', avec des contraintes d'appariement relâchées comparé à l'émission entière-flottante. Sur les deux types précédents de CPU, le reconditionnement des unités était souvent utilisé, mais pas systématique. La moitié des processeurs de l'époque l'utilisaient, pas l'autre. Le reconditionnement de l'AGU aurait pu être utilisé, mais ne l'a été que sur l'Intel i960. Ce qui nous amène à parler des autres CPUs.
D'autres CPU superscalaires de l'époque utilisaient la '''double émission entière''', à savoir qu'ils peuvent émettre une opération en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Pour cela, ils peuvent dupliquer une ALU entière, ou reconditionner l'unité mémoire. Les exemples classiques sont les processeurs Intel : i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 reconditionnait l'unité mémoire en ALU entière.
L'Intel i960 utilisait le reconditionnement de l'AGU en plus du partitionnement, ce qui lui permettait d'exécuter une seconde instruction entière, en plus d'une autre instruction. Les instructions en question étaient simples : additions, quelques décalages. De nos jours, ce combo partitionnement et reconditionnement de l'AGU n'est plus utilisé. Il n'a pas beaucoup de sens quand on peut dupliquer des ALU entières, et les processeurs récents ont un budget en transistor suffisant pour.
Le Pentium dupliquait l'ALU entière, mais n'utilisaient aucune forme de partitionnement. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Contrairement au i960, le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
{|class="wikitable"
|-
! Année !! Processeur !! Nombre de voies !! Nombre d'unités
|-
! rowspan="2" | 1989
| RS/6000 || Double émission entière-flottante || 2 unités : ALU/MEM + FPU
|-
| i960CA || Double émission entière || 2 unités : INT + INT/MEM
|-
! 1991
| Motorola 88110 || Double émission || 10 unités
|-
! rowspan="3" | 1992
| RSC || Double émission entière-flottante || 2 unités : ALU/MEM + FPU
|-
| PA 7100 || Double émission entière-flottante || 2 unités : ALU/MEM + FPU
|-
|21064 || Double émission avec contraintes d'appariement fortes || 3 unités : ALU + FPU + MEM
|-
! rowspan="3" | 1993
| PPC 601 || Double émission entière-flottante || 2 unités : ALU/MEM + FPU
|-
| HyperSPARC || Double émission || 3 unités : ALU + FPU + MEM
|-
| Pentium || Double émission entière || 3 unités : ALU/MEM/FPU + ALU
|}
De rares CPUs ont tenté d'utiliser à la fois double émission entière-flottante et la double émission entière. Le résultat utilisait soit la double, soit la triple émission. Les deux CPUs en question sont le PA-RISC PA 7200 d'HP, et le SuperSPARC.
Le PA 7200, sorti en 1994., est un PA-7100 auquel on aurait rajouté une ALU entière. Précisons que ce processeur utilisait aussi le reconditionnement de la FPU, comme son prédécesseur. Les combinaisons autorisées étaient donc INT + INT, INT + MEM, INT + FLOAT/MUL, MEM + FLOAT/MUL. Les combinaisons autorisées par le partitionnement INT-FLOAT-MEM étaient donc possibles, en plus des combinaisons INT + INT, voire INT + INT + MUL (vu que la FPU est reconditionnée). Autrement dit, il pouvait faire toute ce que permettait un CPU à double émission entière, plus les combinaisons FLOAT/MUL + MEM.
Le SuperSPARC était un CPU triple émission, avec une seconde ALU. Il pouvait émettre, à chaque cycle : maximum deux opérations entières, maximum un accès mémoire, maximum une opération flottante. Le SuperSPARC avait de nombreuses contraintes d’appariement. Par exemple, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Une bonne partie de ces contraintes était lié au banc de registre, qui n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions.
: La seconde ALU a été abandonnée dans l'HyperSPARC, qui est de plus repassé en double émission, ce qui est une évolution à rebours de l'évolution des autres CPUs. Le SuperSPARC était en avance sur son temps, ce quelques années, alors que l'HyperSPARC a un design de son temps.
{|class="wikitable"
|-
! Année !! Processeur !! Nombre de voies !! Nombre d'unités
|-
! 1992
| SuperSPARC || Triple émission avec contraintes d'appariement fortes || 4 unités : 2 ALU + FPU + MEM
|-
! 1994
| PA 7200 || 2 (double émission) || 3 unités : ALU + ALU/MEM + FPU
|}
C'est la même année , en 1994, que la quadruple émission est arrivée et que les CPU ont commencé à avoir de plus en plus d'unités de calcul. La tendance globale était à la duplication de l'ALU entière, combinée au partitionnement INT-FLOAT-MEM. De quoi exécuter en même temps deux opérations entières, une opération flottante, et un accès mémoire. Quelques CPU, comme les CPU alpha, permettaient d'émettre deux opérations flottantes et/ou deux accès mémoire. Même les designs restés en double émission, avaient ajouté des unités de calcul.
L'alpha 21164 était dans ce cas, tout comme le PA-RISC 7200, le SuperSPARC, et le CPU des PowerPC G3 et G4. 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).
{|class="wikitable"
|-
! Année !! Processeur !! Nombre de voies !! Nombre d'unités
|-
! rowspan="2" | 1994
| POWER 2 || 4 (quadruple émission + 2 branchements || 2 ALU/MEM + 2 FPU
|-
| 21164 || 4 (quadruple émission) || 4 unités : 2 ALU + FPU + MEM
|-
! rowspan="5" | 1995
| 603/604 || 2 (double émission) || 3 unités : INT + FPU + MEM
|-
| PA 8000 || 4 (quadruple émission) || 10 unités
|-
| UltraSPARC 1 || 4 (quadruple émission) || 8 unités
|-
| Microarchitecture P6 (+ concurrence AMD) || 2 (double émission) || 5 unités : 2 ALU + 2 FPU + MEM
|-
| MIPS R1000 || 4 (quadruple émission) || 8 unités
|-
! rowspan="2" | 1996
| 620 || 4 (quadruple émission) || 2 ALU + MUL + FPU + MEM
|-
| 21264 || 4 (quadruple émission) || 4 unités : 2 ALU/MEM + 2 FPU (FADD + FMUL)
|-
! ...
| ... || ... || ...
|}
La morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes.
Les CPU superscalaires modernes ont des design légèrement différents des CPU historiques. Ils dupliquent l'unité entière, mais leur partitionnement est différent. En général, les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Par exemple, un CPU à 6 voies pourra émettre/exécuter 6 opérations entières simultanément, grâce à 6 ALU entières. Mais il s'agit là d'un maximum et certaines peuvent être remplacées par une opération flottante ou mémoire, qui utilisent l'unité mémoire et/ou la FPU.
Un exemple assez parlant est les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon. C'était une microarchitecture à triple émission, avec trois ALU entières et trois unités mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements.
Il s'agit là d'une tendance, cependant. Les CPU superscalaires tendent à mettre le paquet niveau opérations arithmétiques, mais pas forcément au point de remplir la totalité de la largeur du processeur. La règle est que pour N voies, le processeur dispose d'au moins N/2 ALU entières. Attention, cela ne signifie pas que le processeur émet systématiquement N/2 opération entières. Il peut par exemple émettre une combinaison d'opérations flottantes et mémoire, avec des branchements. Et N/2 est une borne basse, la plupart des CPU x86 ont environ tournent plutôt autour des 2/3, voire des 3/4.
En général, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. La conséquence est que les unités mémoire sont dupliquées, en seconde place dans l'ordre d'importance. Les processeurs x86 ont commencé à dupliquer leurs unités mémoire dès qu'ils ont atteint la quadruple émission, pour les CPU grand public. La microarchitecture Core d'Intel utilisait la quadruple émission, avec deux unités mémoire et trois ALU entières. Les microarchitectures d'AMD K5 et K6 utilisaient aussi la quadruple émission et avaient deux ALUs pour deux unités mémoire.
Plus récemment, la microarchitecture ''Golden Cove'' dispose de cinq unités de calcul trois unités mémoire (deux spécialisées dans les écritures, trois dans les lectures). La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire.Il y a donc soit égalité, soit avantage pour les ALU entières.
Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair.
==L'unité d'émission d'un processeur superscalaire==
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Voyons pourquoi.
===L'excès de ports d'émission===
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ou 5 ?
Une solution est de réduire la largeur du processeur. Dans l'exemple précédent, il est possible de passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La solution précédente est certes très intéressante, mais elle a des défauts. Le principal est que les ports d'émission sont sous-utilisés. Et les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
===L'intérieur de l'unité d'émission===
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
==Le ''front-end'' des processeurs superscalaires==
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Le banc de registre d'un processeur superscalaire===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Une autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions auto-aligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions auto-aligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez longs, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
kxu3zqmv0dp3ne2sv1p80iohnzje2my
773116
773115
2026-09-25T14:30:52Z
Mewtow
31375
/* L'évolution historique des processeurs superscalaires */
773116
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
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.
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
===L'évolution historique des processeurs superscalaires===
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. La plupart des CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 sont rapidement montés à de la quadruple émission, avec quelques exceptions. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 ||
|-
! 1995
| 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000
|-
! 1996
| class="f_bleu | 620 || || || || class="f_bleu | 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
==Les unités de calcul d'un processeur superscalaire==
Étudier les CPU superscalaires historique était une bonne introduction propédeutique, nous allons maintenant passer aux CPU superscalaires en général. Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Et cela nous permettra de généraliser certains concepts vus sur les CPU superscalaires historiques.
Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est possible d'exploiter plusieurs unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu.
Les tout premiers CPU superscalaires étaient nombreux à utiliser cette technique. Ils étaient des CPU double émission, avec une ALU entière, une unité mémoire et une FPU. Nous parlerons de '''partitionnement INT-FLOAT-MEM'''. Il s'agit de l'HyperSPARC, de l'alpha 21064 et du PowerPC 603. Les contrainte d’appariement variaient grandement suivant le processeur. Les plus permissifs autorisaient toutes les combinaisons possibles entre : une opération entière, une opération flottante, et une unité mémoire. Mais ils n'étaient pas les seuls.
Le PowerPC 603 implémentait ce genre de double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions).
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Le '''reconditionnement d'une unité fonctionnelle''' permet à celle-ci de faire des opérations qu'elle n'est pas prévue pour. Par exemple, il est possible d'utiliser l'unité mémoire comme seconde ALU entière, ou d'utiliser la FPU comme multiplieur. Ces deux exemples ne sont pas choisit au hasard, nous les étudierons dans ce qui suit. Le reconditionnement évite d'avoir à dupliquer une unité de calcul, tout en ajoutant une voie. Il permet d'améliorer l'efficacité du partitionnement, qu'il soit utilisé seul ou non.
Il est possible de reconditionner la FPU pour faire des multiplications entières. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits. Il est possible d'utiliser de multiplieur pour faire des multiplications entières 32 bits. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
Le reconditionnement de la FPU permet d'exécuter une multiplication entière en plus des autres opérations entières. Par exemple, prenons un CPU avec double émission entière-flottante, sans multiplieur entier séparé. Le reconditionnement de la FPU permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine.
Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants.
Passons maintenant à une seconde forme de reconditionnement. Reconditionner l'unité mémoire permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, des versions améliorées de l'Intel 960. L'Intel 960 originel n'était pas superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire au monde. Il intégrait une ALU entière, une unité mémoire et une unité de branchement. Grâce au partitionnement, il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, en remplacement d'une µop mémoire. Son pipeline permettait donc les combinaisons INT + BRANCH + (MEM / + INT*).
Le cas de l'i960 nous montre que le reconditionnement permet d'améliorer l'intérêt du partitionnement. Elle permet de ne pas dupliquer d'unité de calcul, tout en permettant quand même d'émettre une seconde opération entière.
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''.
Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières ! Il doit y avoir deux ALU entières pour de la double émission entière, trois pour de triple émission éntière, 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]]
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications.
Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que dupliquer l'ALU entière peut se marier parfaitement avec la présence d'une FPU. Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU reconditionne sa FPU ou pas. Mais il est très rare qu'un CPU ait à la fois un multiplieur entier et une FPU reconditionnée.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
Le processeur SuperSPARC avait deux ALU entière et une FPU. La FPU était reconditionnée, ce qui fait que la multiplication était faite dans la FPU, idem pour la division. Il lui est possible d'émettre deux opérations entières simples, en plus d'une multiplication. Par contre, on perd la possibilité d'émettre en même temps une multiplication et une opération flottante. Les combinaisons INT + INT + (MUL/FLOAT) sont possibles pas les combinaisons INT + MUL + FPU.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires modernes utilisent toutes ces stratégies===
Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, duplication de l'ALU entière, duplication des autre unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents.
* Le partitionnement a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU.
* Le reconditionnement des unités a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU.
* La duplication des ALU entières est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants.
* La duplication des autres unités a un cout en circuit variable, pour des gains en performance eux aussi variables.
De nos jours, le partitionnement est utilisé en complément de la duplication des ALU entières.
L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. Ces modèles utilisaient presque tous la double émission avec trois unités : une ALU, une FPU et une unité mémoire. Si la moitié des processeurs faisait de la double émission "complète", l'autre moitié utilisait une forme encore plus simple de partitionnement.
La '''double émission entière-flottante''' consiste à émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le processeur a donc deux pipelines : un pipeline entier, et un pipeline flottant. Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire, vu que les µops mémoire passent par l'ALU entière avant d’atterrir dans l'unité mémoire. Le reconditionnement de la FPU était utilisé sur les processeurs à double émission entière-flottante. Il entrainait un léger en performance, pour un cout en circuits presque nul.
{|
|[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU.]]
|[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
|}
Les autres processeurs utilisaient uniquement le reconditionnement, mais séparaient l'unité mémoire de l'ALU entière. Ils faisaient donc du ''partitionnement INT-FLOAT-MEM'', avec des contraintes d'appariement relâchées comparé à l'émission entière-flottante. Sur les deux types précédents de CPU, le reconditionnement des unités était souvent utilisé, mais pas systématique. La moitié des processeurs de l'époque l'utilisaient, pas l'autre. Le reconditionnement de l'AGU aurait pu être utilisé, mais ne l'a été que sur l'Intel i960. Ce qui nous amène à parler des autres CPUs.
D'autres CPU superscalaires de l'époque utilisaient la '''double émission entière''', à savoir qu'ils peuvent émettre une opération en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Pour cela, ils peuvent dupliquer une ALU entière, ou reconditionner l'unité mémoire. Les exemples classiques sont les processeurs Intel : i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 reconditionnait l'unité mémoire en ALU entière.
L'Intel i960 utilisait le reconditionnement de l'AGU en plus du partitionnement, ce qui lui permettait d'exécuter une seconde instruction entière, en plus d'une autre instruction. Les instructions en question étaient simples : additions, quelques décalages. De nos jours, ce combo partitionnement et reconditionnement de l'AGU n'est plus utilisé. Il n'a pas beaucoup de sens quand on peut dupliquer des ALU entières, et les processeurs récents ont un budget en transistor suffisant pour.
Le Pentium dupliquait l'ALU entière, mais n'utilisaient aucune forme de partitionnement. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Contrairement au i960, le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
{|class="wikitable"
|-
! Année !! Processeur !! Nombre de voies !! Nombre d'unités
|-
! rowspan="2" | 1989
| RS/6000 || Double émission entière-flottante || 2 unités : ALU/MEM + FPU
|-
| i960CA || Double émission entière || 2 unités : INT + INT/MEM
|-
! 1991
| Motorola 88110 || Double émission || 10 unités
|-
! rowspan="3" | 1992
| RSC || Double émission entière-flottante || 2 unités : ALU/MEM + FPU
|-
| PA 7100 || Double émission entière-flottante || 2 unités : ALU/MEM + FPU
|-
|21064 || Double émission avec contraintes d'appariement fortes || 3 unités : ALU + FPU + MEM
|-
! rowspan="3" | 1993
| PPC 601 || Double émission entière-flottante || 2 unités : ALU/MEM + FPU
|-
| HyperSPARC || Double émission || 3 unités : ALU + FPU + MEM
|-
| Pentium || Double émission entière || 3 unités : ALU/MEM/FPU + ALU
|}
De rares CPUs ont tenté d'utiliser à la fois double émission entière-flottante et la double émission entière. Le résultat utilisait soit la double, soit la triple émission. Les deux CPUs en question sont le PA-RISC PA 7200 d'HP, et le SuperSPARC.
Le PA 7200, sorti en 1994., est un PA-7100 auquel on aurait rajouté une ALU entière. Précisons que ce processeur utilisait aussi le reconditionnement de la FPU, comme son prédécesseur. Les combinaisons autorisées étaient donc INT + INT, INT + MEM, INT + FLOAT/MUL, MEM + FLOAT/MUL. Les combinaisons autorisées par le partitionnement INT-FLOAT-MEM étaient donc possibles, en plus des combinaisons INT + INT, voire INT + INT + MUL (vu que la FPU est reconditionnée). Autrement dit, il pouvait faire toute ce que permettait un CPU à double émission entière, plus les combinaisons FLOAT/MUL + MEM.
Le SuperSPARC était un CPU triple émission, avec une seconde ALU. Il pouvait émettre, à chaque cycle : maximum deux opérations entières, maximum un accès mémoire, maximum une opération flottante. Le SuperSPARC avait de nombreuses contraintes d’appariement. Par exemple, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Une bonne partie de ces contraintes était lié au banc de registre, qui n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions.
: La seconde ALU a été abandonnée dans l'HyperSPARC, qui est de plus repassé en double émission, ce qui est une évolution à rebours de l'évolution des autres CPUs. Le SuperSPARC était en avance sur son temps, ce quelques années, alors que l'HyperSPARC a un design de son temps.
{|class="wikitable"
|-
! Année !! Processeur !! Nombre de voies !! Nombre d'unités
|-
! 1992
| SuperSPARC || Triple émission avec contraintes d'appariement fortes || 4 unités : 2 ALU + FPU + MEM
|-
! 1994
| PA 7200 || 2 (double émission) || 3 unités : ALU + ALU/MEM + FPU
|}
C'est la même année , en 1994, que la quadruple émission est arrivée et que les CPU ont commencé à avoir de plus en plus d'unités de calcul. La tendance globale était à la duplication de l'ALU entière, combinée au partitionnement INT-FLOAT-MEM. De quoi exécuter en même temps deux opérations entières, une opération flottante, et un accès mémoire. Quelques CPU, comme les CPU alpha, permettaient d'émettre deux opérations flottantes et/ou deux accès mémoire. Même les designs restés en double émission, avaient ajouté des unités de calcul.
L'alpha 21164 était dans ce cas, tout comme le PA-RISC 7200, le SuperSPARC, et le CPU des PowerPC G3 et G4. 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).
{|class="wikitable"
|-
! Année !! Processeur !! Nombre de voies !! Nombre d'unités
|-
! rowspan="2" | 1994
| POWER 2 || 4 (quadruple émission + 2 branchements || 2 ALU/MEM + 2 FPU
|-
| 21164 || 4 (quadruple émission) || 4 unités : 2 ALU + FPU + MEM
|-
! rowspan="5" | 1995
| 603/604 || 2 (double émission) || 3 unités : INT + FPU + MEM
|-
| PA 8000 || 4 (quadruple émission) || 10 unités
|-
| UltraSPARC 1 || 4 (quadruple émission) || 8 unités
|-
| Microarchitecture P6 (+ concurrence AMD) || 2 (double émission) || 5 unités : 2 ALU + 2 FPU + MEM
|-
| MIPS R1000 || 4 (quadruple émission) || 8 unités
|-
! rowspan="2" | 1996
| 620 || 4 (quadruple émission) || 2 ALU + MUL + FPU + MEM
|-
| 21264 || 4 (quadruple émission) || 4 unités : 2 ALU/MEM + 2 FPU (FADD + FMUL)
|-
! ...
| ... || ... || ...
|}
La morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes.
Les CPU superscalaires modernes ont des design légèrement différents des CPU historiques. Ils dupliquent l'unité entière, mais leur partitionnement est différent. En général, les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Par exemple, un CPU à 6 voies pourra émettre/exécuter 6 opérations entières simultanément, grâce à 6 ALU entières. Mais il s'agit là d'un maximum et certaines peuvent être remplacées par une opération flottante ou mémoire, qui utilisent l'unité mémoire et/ou la FPU.
Un exemple assez parlant est les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon. C'était une microarchitecture à triple émission, avec trois ALU entières et trois unités mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements.
Il s'agit là d'une tendance, cependant. Les CPU superscalaires tendent à mettre le paquet niveau opérations arithmétiques, mais pas forcément au point de remplir la totalité de la largeur du processeur. La règle est que pour N voies, le processeur dispose d'au moins N/2 ALU entières. Attention, cela ne signifie pas que le processeur émet systématiquement N/2 opération entières. Il peut par exemple émettre une combinaison d'opérations flottantes et mémoire, avec des branchements. Et N/2 est une borne basse, la plupart des CPU x86 ont environ tournent plutôt autour des 2/3, voire des 3/4.
En général, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. La conséquence est que les unités mémoire sont dupliquées, en seconde place dans l'ordre d'importance. Les processeurs x86 ont commencé à dupliquer leurs unités mémoire dès qu'ils ont atteint la quadruple émission, pour les CPU grand public. La microarchitecture Core d'Intel utilisait la quadruple émission, avec deux unités mémoire et trois ALU entières. Les microarchitectures d'AMD K5 et K6 utilisaient aussi la quadruple émission et avaient deux ALUs pour deux unités mémoire.
Plus récemment, la microarchitecture ''Golden Cove'' dispose de cinq unités de calcul trois unités mémoire (deux spécialisées dans les écritures, trois dans les lectures). La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire.Il y a donc soit égalité, soit avantage pour les ALU entières.
Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair.
==L'unité d'émission d'un processeur superscalaire==
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Voyons pourquoi.
===L'excès de ports d'émission===
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ou 5 ?
Une solution est de réduire la largeur du processeur. Dans l'exemple précédent, il est possible de passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La solution précédente est certes très intéressante, mais elle a des défauts. Le principal est que les ports d'émission sont sous-utilisés. Et les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
===L'intérieur de l'unité d'émission===
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
==Le ''front-end'' des processeurs superscalaires==
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Le banc de registre d'un processeur superscalaire===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Une autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions auto-aligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions auto-aligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez longs, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
7m2wg29k3g1hx9e658kc2q87kmitn73
773117
773116
2026-09-25T14:31:45Z
Mewtow
31375
/* Les CPU superscalaires modernes utilisent toutes ces stratégies */
773117
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
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.
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
===L'évolution historique des processeurs superscalaires===
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. La plupart des CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 sont rapidement montés à de la quadruple émission, avec quelques exceptions. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 ||
|-
! 1995
| 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000
|-
! 1996
| class="f_bleu | 620 || || || || class="f_bleu | 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
==Les unités de calcul d'un processeur superscalaire==
Étudier les CPU superscalaires historique était une bonne introduction propédeutique, nous allons maintenant passer aux CPU superscalaires en général. Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Et cela nous permettra de généraliser certains concepts vus sur les CPU superscalaires historiques.
Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est possible d'exploiter plusieurs unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu.
Les tout premiers CPU superscalaires étaient nombreux à utiliser cette technique. Ils étaient des CPU double émission, avec une ALU entière, une unité mémoire et une FPU. Nous parlerons de '''partitionnement INT-FLOAT-MEM'''. Il s'agit de l'HyperSPARC, de l'alpha 21064 et du PowerPC 603. Les contrainte d’appariement variaient grandement suivant le processeur. Les plus permissifs autorisaient toutes les combinaisons possibles entre : une opération entière, une opération flottante, et une unité mémoire. Mais ils n'étaient pas les seuls.
Le PowerPC 603 implémentait ce genre de double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions).
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Le '''reconditionnement d'une unité fonctionnelle''' permet à celle-ci de faire des opérations qu'elle n'est pas prévue pour. Par exemple, il est possible d'utiliser l'unité mémoire comme seconde ALU entière, ou d'utiliser la FPU comme multiplieur. Ces deux exemples ne sont pas choisit au hasard, nous les étudierons dans ce qui suit. Le reconditionnement évite d'avoir à dupliquer une unité de calcul, tout en ajoutant une voie. Il permet d'améliorer l'efficacité du partitionnement, qu'il soit utilisé seul ou non.
Il est possible de reconditionner la FPU pour faire des multiplications entières. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits. Il est possible d'utiliser de multiplieur pour faire des multiplications entières 32 bits. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
Le reconditionnement de la FPU permet d'exécuter une multiplication entière en plus des autres opérations entières. Par exemple, prenons un CPU avec double émission entière-flottante, sans multiplieur entier séparé. Le reconditionnement de la FPU permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine.
Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants.
Passons maintenant à une seconde forme de reconditionnement. Reconditionner l'unité mémoire permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, des versions améliorées de l'Intel 960. L'Intel 960 originel n'était pas superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire au monde. Il intégrait une ALU entière, une unité mémoire et une unité de branchement. Grâce au partitionnement, il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, en remplacement d'une µop mémoire. Son pipeline permettait donc les combinaisons INT + BRANCH + (MEM / + INT*).
Le cas de l'i960 nous montre que le reconditionnement permet d'améliorer l'intérêt du partitionnement. Elle permet de ne pas dupliquer d'unité de calcul, tout en permettant quand même d'émettre une seconde opération entière.
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''.
Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières ! Il doit y avoir deux ALU entières pour de la double émission entière, trois pour de triple émission éntière, 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]]
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications.
Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que dupliquer l'ALU entière peut se marier parfaitement avec la présence d'une FPU. Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU reconditionne sa FPU ou pas. Mais il est très rare qu'un CPU ait à la fois un multiplieur entier et une FPU reconditionnée.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
Le processeur SuperSPARC avait deux ALU entière et une FPU. La FPU était reconditionnée, ce qui fait que la multiplication était faite dans la FPU, idem pour la division. Il lui est possible d'émettre deux opérations entières simples, en plus d'une multiplication. Par contre, on perd la possibilité d'émettre en même temps une multiplication et une opération flottante. Les combinaisons INT + INT + (MUL/FLOAT) sont possibles pas les combinaisons INT + MUL + FPU.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires modernes utilisent toutes ces stratégies===
Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, duplication de l'ALU entière, duplication des autre unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents.
* Le partitionnement a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU.
* Le reconditionnement des unités a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU.
* La duplication des ALU entières est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants.
* La duplication des autres unités a un cout en circuit variable, pour des gains en performance eux aussi variables.
De nos jours, le partitionnement est utilisé en complément de la duplication des ALU entières, et de la duplication des autres unités. Mais les anciens processeurs superscalaires faisaient autrement.
L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. Ces modèles utilisaient presque tous la double émission avec trois unités : une ALU, une FPU et une unité mémoire. Si la moitié des processeurs faisait de la double émission "complète", l'autre moitié utilisait une forme encore plus simple de partitionnement.
La '''double émission entière-flottante''' consiste à émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le processeur a donc deux pipelines : un pipeline entier, et un pipeline flottant. Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire, vu que les µops mémoire passent par l'ALU entière avant d’atterrir dans l'unité mémoire. Le reconditionnement de la FPU était utilisé sur les processeurs à double émission entière-flottante. Il entrainait un léger en performance, pour un cout en circuits presque nul.
{|
|[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU.]]
|[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
|}
Les autres processeurs utilisaient uniquement le reconditionnement, mais séparaient l'unité mémoire de l'ALU entière. Ils faisaient donc du ''partitionnement INT-FLOAT-MEM'', avec des contraintes d'appariement relâchées comparé à l'émission entière-flottante. Sur les deux types précédents de CPU, le reconditionnement des unités était souvent utilisé, mais pas systématique. La moitié des processeurs de l'époque l'utilisaient, pas l'autre. Le reconditionnement de l'AGU aurait pu être utilisé, mais ne l'a été que sur l'Intel i960. Ce qui nous amène à parler des autres CPUs.
D'autres CPU superscalaires de l'époque utilisaient la '''double émission entière''', à savoir qu'ils peuvent émettre une opération en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Pour cela, ils peuvent dupliquer une ALU entière, ou reconditionner l'unité mémoire. Les exemples classiques sont les processeurs Intel : i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 reconditionnait l'unité mémoire en ALU entière.
L'Intel i960 utilisait le reconditionnement de l'AGU en plus du partitionnement, ce qui lui permettait d'exécuter une seconde instruction entière, en plus d'une autre instruction. Les instructions en question étaient simples : additions, quelques décalages. De nos jours, ce combo partitionnement et reconditionnement de l'AGU n'est plus utilisé. Il n'a pas beaucoup de sens quand on peut dupliquer des ALU entières, et les processeurs récents ont un budget en transistor suffisant pour.
Le Pentium dupliquait l'ALU entière, mais n'utilisaient aucune forme de partitionnement. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Contrairement au i960, le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
{|class="wikitable"
|-
! Année !! Processeur !! Nombre de voies !! Nombre d'unités
|-
! rowspan="2" | 1989
| RS/6000 || Double émission entière-flottante || 2 unités : ALU/MEM + FPU
|-
| i960CA || Double émission entière || 2 unités : INT + INT/MEM
|-
! 1991
| Motorola 88110 || Double émission || 10 unités
|-
! rowspan="3" | 1992
| RSC || Double émission entière-flottante || 2 unités : ALU/MEM + FPU
|-
| PA 7100 || Double émission entière-flottante || 2 unités : ALU/MEM + FPU
|-
|21064 || Double émission avec contraintes d'appariement fortes || 3 unités : ALU + FPU + MEM
|-
! rowspan="3" | 1993
| PPC 601 || Double émission entière-flottante || 2 unités : ALU/MEM + FPU
|-
| HyperSPARC || Double émission || 3 unités : ALU + FPU + MEM
|-
| Pentium || Double émission entière || 3 unités : ALU/MEM/FPU + ALU
|}
De rares CPUs ont tenté d'utiliser à la fois double émission entière-flottante et la double émission entière. Le résultat utilisait soit la double, soit la triple émission. Les deux CPUs en question sont le PA-RISC PA 7200 d'HP, et le SuperSPARC.
Le PA 7200, sorti en 1994., est un PA-7100 auquel on aurait rajouté une ALU entière. Précisons que ce processeur utilisait aussi le reconditionnement de la FPU, comme son prédécesseur. Les combinaisons autorisées étaient donc INT + INT, INT + MEM, INT + FLOAT/MUL, MEM + FLOAT/MUL. Les combinaisons autorisées par le partitionnement INT-FLOAT-MEM étaient donc possibles, en plus des combinaisons INT + INT, voire INT + INT + MUL (vu que la FPU est reconditionnée). Autrement dit, il pouvait faire toute ce que permettait un CPU à double émission entière, plus les combinaisons FLOAT/MUL + MEM.
Le SuperSPARC était un CPU triple émission, avec une seconde ALU. Il pouvait émettre, à chaque cycle : maximum deux opérations entières, maximum un accès mémoire, maximum une opération flottante. Le SuperSPARC avait de nombreuses contraintes d’appariement. Par exemple, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Une bonne partie de ces contraintes était lié au banc de registre, qui n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions.
: La seconde ALU a été abandonnée dans l'HyperSPARC, qui est de plus repassé en double émission, ce qui est une évolution à rebours de l'évolution des autres CPUs. Le SuperSPARC était en avance sur son temps, ce quelques années, alors que l'HyperSPARC a un design de son temps.
{|class="wikitable"
|-
! Année !! Processeur !! Nombre de voies !! Nombre d'unités
|-
! 1992
| SuperSPARC || Triple émission avec contraintes d'appariement fortes || 4 unités : 2 ALU + FPU + MEM
|-
! 1994
| PA 7200 || 2 (double émission) || 3 unités : ALU + ALU/MEM + FPU
|}
C'est la même année , en 1994, que la quadruple émission est arrivée et que les CPU ont commencé à avoir de plus en plus d'unités de calcul. La tendance globale était à la duplication de l'ALU entière, combinée au partitionnement INT-FLOAT-MEM. De quoi exécuter en même temps deux opérations entières, une opération flottante, et un accès mémoire. Quelques CPU, comme les CPU alpha, permettaient d'émettre deux opérations flottantes et/ou deux accès mémoire. Même les designs restés en double émission, avaient ajouté des unités de calcul.
L'alpha 21164 était dans ce cas, tout comme le PA-RISC 7200, le SuperSPARC, et le CPU des PowerPC G3 et G4. 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).
{|class="wikitable"
|-
! Année !! Processeur !! Nombre de voies !! Nombre d'unités
|-
! rowspan="2" | 1994
| POWER 2 || 4 (quadruple émission + 2 branchements || 2 ALU/MEM + 2 FPU
|-
| 21164 || 4 (quadruple émission) || 4 unités : 2 ALU + FPU + MEM
|-
! rowspan="5" | 1995
| 603/604 || 2 (double émission) || 3 unités : INT + FPU + MEM
|-
| PA 8000 || 4 (quadruple émission) || 10 unités
|-
| UltraSPARC 1 || 4 (quadruple émission) || 8 unités
|-
| Microarchitecture P6 (+ concurrence AMD) || 2 (double émission) || 5 unités : 2 ALU + 2 FPU + MEM
|-
| MIPS R1000 || 4 (quadruple émission) || 8 unités
|-
! rowspan="2" | 1996
| 620 || 4 (quadruple émission) || 2 ALU + MUL + FPU + MEM
|-
| 21264 || 4 (quadruple émission) || 4 unités : 2 ALU/MEM + 2 FPU (FADD + FMUL)
|-
! ...
| ... || ... || ...
|}
La morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes.
Les CPU superscalaires modernes ont des design légèrement différents des CPU historiques. Ils dupliquent l'unité entière, mais leur partitionnement est différent. En général, les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Par exemple, un CPU à 6 voies pourra émettre/exécuter 6 opérations entières simultanément, grâce à 6 ALU entières. Mais il s'agit là d'un maximum et certaines peuvent être remplacées par une opération flottante ou mémoire, qui utilisent l'unité mémoire et/ou la FPU.
Un exemple assez parlant est les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon. C'était une microarchitecture à triple émission, avec trois ALU entières et trois unités mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements.
Il s'agit là d'une tendance, cependant. Les CPU superscalaires tendent à mettre le paquet niveau opérations arithmétiques, mais pas forcément au point de remplir la totalité de la largeur du processeur. La règle est que pour N voies, le processeur dispose d'au moins N/2 ALU entières. Attention, cela ne signifie pas que le processeur émet systématiquement N/2 opération entières. Il peut par exemple émettre une combinaison d'opérations flottantes et mémoire, avec des branchements. Et N/2 est une borne basse, la plupart des CPU x86 ont environ tournent plutôt autour des 2/3, voire des 3/4.
En général, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. La conséquence est que les unités mémoire sont dupliquées, en seconde place dans l'ordre d'importance. Les processeurs x86 ont commencé à dupliquer leurs unités mémoire dès qu'ils ont atteint la quadruple émission, pour les CPU grand public. La microarchitecture Core d'Intel utilisait la quadruple émission, avec deux unités mémoire et trois ALU entières. Les microarchitectures d'AMD K5 et K6 utilisaient aussi la quadruple émission et avaient deux ALUs pour deux unités mémoire.
Plus récemment, la microarchitecture ''Golden Cove'' dispose de cinq unités de calcul trois unités mémoire (deux spécialisées dans les écritures, trois dans les lectures). La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire.Il y a donc soit égalité, soit avantage pour les ALU entières.
Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair.
==L'unité d'émission d'un processeur superscalaire==
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Voyons pourquoi.
===L'excès de ports d'émission===
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ou 5 ?
Une solution est de réduire la largeur du processeur. Dans l'exemple précédent, il est possible de passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La solution précédente est certes très intéressante, mais elle a des défauts. Le principal est que les ports d'émission sont sous-utilisés. Et les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
===L'intérieur de l'unité d'émission===
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
==Le ''front-end'' des processeurs superscalaires==
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Le banc de registre d'un processeur superscalaire===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Une autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions auto-aligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions auto-aligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez longs, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
prjp3ybbhn8gjpwq65wsayr84zb3ogz
773118
773117
2026-09-25T14:54:56Z
Mewtow
31375
/* Les CPU superscalaires modernes utilisent toutes ces stratégies */
773118
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
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.
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
===L'évolution historique des processeurs superscalaires===
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. La plupart des CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 sont rapidement montés à de la quadruple émission, avec quelques exceptions. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 ||
|-
! 1995
| 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000
|-
! 1996
| class="f_bleu | 620 || || || || class="f_bleu | 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
==Les unités de calcul d'un processeur superscalaire==
Étudier les CPU superscalaires historique était une bonne introduction propédeutique, nous allons maintenant passer aux CPU superscalaires en général. Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Et cela nous permettra de généraliser certains concepts vus sur les CPU superscalaires historiques.
Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est possible d'exploiter plusieurs unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu.
Les tout premiers CPU superscalaires étaient nombreux à utiliser cette technique. Ils étaient des CPU double émission, avec une ALU entière, une unité mémoire et une FPU. Nous parlerons de '''partitionnement INT-FLOAT-MEM'''. Il s'agit de l'HyperSPARC, de l'alpha 21064 et du PowerPC 603. Les contrainte d’appariement variaient grandement suivant le processeur. Les plus permissifs autorisaient toutes les combinaisons possibles entre : une opération entière, une opération flottante, et une unité mémoire. Mais ils n'étaient pas les seuls.
Le PowerPC 603 implémentait ce genre de double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions).
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Le '''reconditionnement d'une unité fonctionnelle''' permet à celle-ci de faire des opérations qu'elle n'est pas prévue pour. Par exemple, il est possible d'utiliser l'unité mémoire comme seconde ALU entière, ou d'utiliser la FPU comme multiplieur. Ces deux exemples ne sont pas choisit au hasard, nous les étudierons dans ce qui suit. Le reconditionnement évite d'avoir à dupliquer une unité de calcul, tout en ajoutant une voie. Il permet d'améliorer l'efficacité du partitionnement, qu'il soit utilisé seul ou non.
Il est possible de reconditionner la FPU pour faire des multiplications entières. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits. Il est possible d'utiliser de multiplieur pour faire des multiplications entières 32 bits. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
Le reconditionnement de la FPU permet d'exécuter une multiplication entière en plus des autres opérations entières. Par exemple, prenons un CPU avec double émission entière-flottante, sans multiplieur entier séparé. Le reconditionnement de la FPU permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine.
Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants.
Passons maintenant à une seconde forme de reconditionnement. Reconditionner l'unité mémoire permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, des versions améliorées de l'Intel 960. L'Intel 960 originel n'était pas superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire au monde. Il intégrait une ALU entière, une unité mémoire et une unité de branchement. Grâce au partitionnement, il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, en remplacement d'une µop mémoire. Son pipeline permettait donc les combinaisons INT + BRANCH + (MEM / + INT*).
Le cas de l'i960 nous montre que le reconditionnement permet d'améliorer l'intérêt du partitionnement. Elle permet de ne pas dupliquer d'unité de calcul, tout en permettant quand même d'émettre une seconde opération entière.
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''.
Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières ! Il doit y avoir deux ALU entières pour de la double émission entière, trois pour de triple émission éntière, 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]]
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications.
Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que dupliquer l'ALU entière peut se marier parfaitement avec la présence d'une FPU. Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU reconditionne sa FPU ou pas. Mais il est très rare qu'un CPU ait à la fois un multiplieur entier et une FPU reconditionnée.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
Le processeur SuperSPARC avait deux ALU entière et une FPU. La FPU était reconditionnée, ce qui fait que la multiplication était faite dans la FPU, idem pour la division. Il lui est possible d'émettre deux opérations entières simples, en plus d'une multiplication. Par contre, on perd la possibilité d'émettre en même temps une multiplication et une opération flottante. Les combinaisons INT + INT + (MUL/FLOAT) sont possibles pas les combinaisons INT + MUL + FPU.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires modernes utilisent toutes ces stratégies===
Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, duplication de l'ALU entière, duplication des autre unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents.
* Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU.
* Le '''reconditionnement des unités''' a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU.
* La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants.
* La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables.
De nos jours, le partitionnement est utilisé en complément de la duplication des ALU entières, et de la duplication des autres unités. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. Ces modèles utilisaient presque tous la double émission avec trois unités : une ALU, une FPU et une unité mémoire. Si la moitié des processeurs faisait de la double émission "complète", l'autre moitié utilisait une forme encore plus simple de partitionnement.
Pour commencer, étudions le cas des premiers CPU superscalaires à double émission. Ils sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Notamment, la duplication des autres unités ne sert pour ainsi dire à rien, ce qui limite les possibilités, mais rendra les explications plus simples.
La '''double émission entière-flottante''' consiste à émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le processeur a donc deux pipelines : un pipeline entier, et un pipeline flottant. Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire, vu que les µops mémoire passent par l'ALU entière avant d’atterrir dans l'unité mémoire. Le reconditionnement de la FPU était utilisé sur les processeurs à double émission entière-flottante. Il entrainait un léger en performance, pour un cout en circuits presque nul.
{|
|[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU.]]
|[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
|}
D'autres processeurs utilisaient uniquement le reconditionnement, mais séparaient l'unité mémoire de l'ALU entière. Ils faisaient donc du '''partitionnement INT-FLOAT-MEM''', avec des contraintes d'appariement relâchées comparé à l'émission entière-flottante. Sur les deux types précédents de CPU, le reconditionnement des unités était souvent utilisé, mais pas systématique. La moitié des processeurs de l'époque l'utilisaient, pas l'autre. Le reconditionnement de l'AGU aurait pu être utilisé, mais ne l'a été que sur l'Intel i960. Ce qui nous amène à parler des autres CPUs.
D'autres CPU superscalaires de l'époque utilisaient la '''double émission entière''', à savoir qu'ils peuvent émettre une opération en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Pour cela, ils peuvent dupliquer une ALU entière, ou reconditionner l'unité mémoire. Les exemples classiques sont les processeurs Intel : i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 reconditionnait l'unité mémoire en ALU entière.
L'Intel i960 utilisait le reconditionnement de l'AGU en plus du partitionnement, ce qui lui permettait d'exécuter une seconde instruction entière, en plus d'une autre instruction. Les instructions en question étaient simples : additions, quelques décalages. De nos jours, ce combo partitionnement et reconditionnement de l'AGU n'est plus utilisé. Il n'a pas beaucoup de sens quand on peut dupliquer des ALU entières, et les processeurs récents ont un budget en transistor suffisant pour.
Le Pentium dupliquait l'ALU entière, mais n'utilisaient aucune forme de partitionnement. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Contrairement au i960, le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
De rares CPUs ont tenté d'utiliser à la fois le partitionnement et la double émission entière. Le résultat utilisait soit la double, soit la triple émission. Les deux CPUs en question sont le PA-RISC PA 7200 d'HP, et le SuperSPARC.
Le PA 7200, sorti en 1994, est un PA-7100 auquel on aurait rajouté une ALU entière. Précisons que ce processeur utilisait aussi le reconditionnement de la FPU, comme son prédécesseur. Les combinaisons autorisées étaient donc INT + INT, INT + MEM, INT + FLOAT/MUL, MEM + FLOAT/MUL. Les combinaisons autorisées par le partitionnement INT-FLOAT-MEM étaient donc possibles, en plus des combinaisons INT + INT, voire INT + INT + MUL (vu que la FPU est reconditionnée). Autrement dit, il pouvait faire toute ce que permettait un CPU à double émission entière, plus les combinaisons FLOAT/MUL + MEM.
Le SuperSPARC était un CPU triple émission, avec deux ALU entières. Il pouvait émettre, à chaque cycle, trois µops en piochant dans deux opérations entières, un accès mémoire, une opération flottante. Le SuperSPARC avait de nombreuses contraintes d’appariement. Par exemple, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Une bonne partie de ces contraintes était lié au banc de registre, qui n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions.
: La seconde ALU a été abandonnée dans l'HyperSPARC, qui est de plus repassé en double émission, ce qui est une évolution à rebours de l'évolution des autres CPUs. Le SuperSPARC était en avance sur son temps, ce quelques années, alors que l'HyperSPARC a un design de son temps.
Le Motorola 88110 était un CPU double émission avec pas moins de 10 unités : deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise.
Pour résumer, voici le tableau chronologique disant quel CPU utilisait quelle technique. Les CPU barrés sont des COU quadruple émission.
* Le partitionnement INT-FLOAT-MEM est en jaune.
* La double émission entière-flottante est en vert.
* La double émission entière est en rouge.
* Les processeurs sans couleur utilisaient soit de la triple émission, soit de la double émission avec un mélange de plusieurs techniques précédentes. Typiquement, ils répartissaient deux µops sur un grand nombre d'unités distinctes.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| class="f_vert | RS/6000 || || || class="f_rouge | i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| class="f_vert | RSC || class="f_vert | PA 7100 || SuperSPARC || || class="f_jaune | 21064 ||
|-
! 1993
| class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || ||
|-
! 1994
| class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 ||
|-
! 1995
| 603/604 || <s>PA 8000</s> || <s>UltraSPARC 1</s> || Microarchitecture P6 (+ concurrence AMD) || || <s>MIPS R1000</s>
|}
C'est la même année , en 1994, que la quadruple émission est arrivée et que les CPU ont commencé à avoir de plus en plus d'unités de calcul. La tendance globale était à la duplication de l'ALU entière, combinée au partitionnement INT-FLOAT-MEM. De quoi exécuter en même temps deux opérations entières, une opération flottante, et un accès mémoire. Quelques CPU, comme les CPU alpha, permettaient d'émettre deux opérations flottantes et/ou deux accès mémoire. Même les designs restés en double émission, avaient ajouté des unités de calcul.
L'alpha 21164 était dans ce cas, tout comme le PA-RISC 7200, le SuperSPARC, et le CPU des PowerPC G3 et G4. 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).
{|class="wikitable"
|-
! Année !! Processeur !! Nombre de voies !! Nombre d'unités
|-
! rowspan="2" | 1994
| POWER 2 || 4 (quadruple émission + 2 branchements || 2 ALU/MEM + 2 FPU
|-
| 21164 || 4 (quadruple émission) || 4 unités : 2 ALU + FPU + MEM
|-
! rowspan="5" | 1995
| 603/604 || 2 (double émission) || 3 unités : INT + FPU + MEM
|-
| PA 8000 || 4 (quadruple émission) || 10 unités
|-
| UltraSPARC 1 || 4 (quadruple émission) || 8 unités
|-
| Microarchitecture P6 (+ concurrence AMD) || 2 (double émission) || 5 unités : 2 ALU + 2 FPU + MEM
|-
| MIPS R1000 || 4 (quadruple émission) || 8 unités
|-
! rowspan="2" | 1996
| 620 || 4 (quadruple émission) || 2 ALU + MUL + FPU + MEM
|-
| 21264 || 4 (quadruple émission) || 4 unités : 2 ALU/MEM + 2 FPU (FADD + FMUL)
|-
! ...
| ... || ... || ...
|}
La morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes.
Les CPU superscalaires modernes ont des design légèrement différents des CPU historiques. Ils dupliquent l'unité entière, mais leur partitionnement est différent. En général, les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Par exemple, un CPU à 6 voies pourra émettre/exécuter 6 opérations entières simultanément, grâce à 6 ALU entières. Mais il s'agit là d'un maximum et certaines peuvent être remplacées par une opération flottante ou mémoire, qui utilisent l'unité mémoire et/ou la FPU.
Un exemple assez parlant est les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon. C'était une microarchitecture à triple émission, avec trois ALU entières et trois unités mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements.
Il s'agit là d'une tendance, cependant. Les CPU superscalaires tendent à mettre le paquet niveau opérations arithmétiques, mais pas forcément au point de remplir la totalité de la largeur du processeur. La règle est que pour N voies, le processeur dispose d'au moins N/2 ALU entières. Attention, cela ne signifie pas que le processeur émet systématiquement N/2 opération entières. Il peut par exemple émettre une combinaison d'opérations flottantes et mémoire, avec des branchements. Et N/2 est une borne basse, la plupart des CPU x86 ont environ tournent plutôt autour des 2/3, voire des 3/4.
En général, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. La conséquence est que les unités mémoire sont dupliquées, en seconde place dans l'ordre d'importance. Les processeurs x86 ont commencé à dupliquer leurs unités mémoire dès qu'ils ont atteint la quadruple émission, pour les CPU grand public. La microarchitecture Core d'Intel utilisait la quadruple émission, avec deux unités mémoire et trois ALU entières. Les microarchitectures d'AMD K5 et K6 utilisaient aussi la quadruple émission et avaient deux ALUs pour deux unités mémoire.
Plus récemment, la microarchitecture ''Golden Cove'' dispose de cinq unités de calcul trois unités mémoire (deux spécialisées dans les écritures, trois dans les lectures). La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire.Il y a donc soit égalité, soit avantage pour les ALU entières.
Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair.
==L'unité d'émission d'un processeur superscalaire==
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Voyons pourquoi.
===L'excès de ports d'émission===
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ou 5 ?
Une solution est de réduire la largeur du processeur. Dans l'exemple précédent, il est possible de passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La solution précédente est certes très intéressante, mais elle a des défauts. Le principal est que les ports d'émission sont sous-utilisés. Et les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
===L'intérieur de l'unité d'émission===
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
==Le ''front-end'' des processeurs superscalaires==
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Le banc de registre d'un processeur superscalaire===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Une autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions auto-aligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions auto-aligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez longs, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
2bnk3thdw33muzpjp543t1zfpgj4t45
773119
773118
2026-09-25T14:57:03Z
Mewtow
31375
/* Les CPU superscalaires modernes utilisent toutes ces stratégies */
773119
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
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.
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
===L'évolution historique des processeurs superscalaires===
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. La plupart des CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 sont rapidement montés à de la quadruple émission, avec quelques exceptions. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 ||
|-
! 1995
| 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000
|-
! 1996
| class="f_bleu | 620 || || || || class="f_bleu | 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
==Les unités de calcul d'un processeur superscalaire==
Étudier les CPU superscalaires historique était une bonne introduction propédeutique, nous allons maintenant passer aux CPU superscalaires en général. Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Et cela nous permettra de généraliser certains concepts vus sur les CPU superscalaires historiques.
Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est possible d'exploiter plusieurs unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu.
Les tout premiers CPU superscalaires étaient nombreux à utiliser cette technique. Ils étaient des CPU double émission, avec une ALU entière, une unité mémoire et une FPU. Nous parlerons de '''partitionnement INT-FLOAT-MEM'''. Il s'agit de l'HyperSPARC, de l'alpha 21064 et du PowerPC 603. Les contrainte d’appariement variaient grandement suivant le processeur. Les plus permissifs autorisaient toutes les combinaisons possibles entre : une opération entière, une opération flottante, et une unité mémoire. Mais ils n'étaient pas les seuls.
Le PowerPC 603 implémentait ce genre de double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions).
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Le '''reconditionnement d'une unité fonctionnelle''' permet à celle-ci de faire des opérations qu'elle n'est pas prévue pour. Par exemple, il est possible d'utiliser l'unité mémoire comme seconde ALU entière, ou d'utiliser la FPU comme multiplieur. Ces deux exemples ne sont pas choisit au hasard, nous les étudierons dans ce qui suit. Le reconditionnement évite d'avoir à dupliquer une unité de calcul, tout en ajoutant une voie. Il permet d'améliorer l'efficacité du partitionnement, qu'il soit utilisé seul ou non.
Il est possible de reconditionner la FPU pour faire des multiplications entières. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits. Il est possible d'utiliser de multiplieur pour faire des multiplications entières 32 bits. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
Le reconditionnement de la FPU permet d'exécuter une multiplication entière en plus des autres opérations entières. Par exemple, prenons un CPU avec double émission entière-flottante, sans multiplieur entier séparé. Le reconditionnement de la FPU permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine.
Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants.
Passons maintenant à une seconde forme de reconditionnement. Reconditionner l'unité mémoire permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, des versions améliorées de l'Intel 960. L'Intel 960 originel n'était pas superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire au monde. Il intégrait une ALU entière, une unité mémoire et une unité de branchement. Grâce au partitionnement, il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, en remplacement d'une µop mémoire. Son pipeline permettait donc les combinaisons INT + BRANCH + (MEM / + INT*).
Le cas de l'i960 nous montre que le reconditionnement permet d'améliorer l'intérêt du partitionnement. Elle permet de ne pas dupliquer d'unité de calcul, tout en permettant quand même d'émettre une seconde opération entière.
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''.
Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières ! Il doit y avoir deux ALU entières pour de la double émission entière, trois pour de triple émission éntière, 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]]
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications.
Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que dupliquer l'ALU entière peut se marier parfaitement avec la présence d'une FPU. Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU reconditionne sa FPU ou pas. Mais il est très rare qu'un CPU ait à la fois un multiplieur entier et une FPU reconditionnée.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
Le processeur SuperSPARC avait deux ALU entière et une FPU. La FPU était reconditionnée, ce qui fait que la multiplication était faite dans la FPU, idem pour la division. Il lui est possible d'émettre deux opérations entières simples, en plus d'une multiplication. Par contre, on perd la possibilité d'émettre en même temps une multiplication et une opération flottante. Les combinaisons INT + INT + (MUL/FLOAT) sont possibles pas les combinaisons INT + MUL + FPU.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires modernes utilisent toutes ces stratégies===
Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, duplication de l'ALU entière, duplication des autre unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents.
* Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU.
* Le '''reconditionnement des unités''' a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU.
* La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants.
* La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables.
De nos jours, le partitionnement est utilisé en complément de la duplication des ALU entières, et de la duplication des autres unités. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. Ces modèles utilisaient presque tous la double émission avec trois unités : une ALU, une FPU et une unité mémoire. Si la moitié des processeurs faisait de la double émission "complète", l'autre moitié utilisait une forme encore plus simple de partitionnement.
Pour commencer, étudions le cas des premiers CPU superscalaires à double émission. Ils sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Notamment, la duplication des autres unités ne sert pour ainsi dire à rien, ce qui limite les possibilités, mais rendra les explications plus simples.
La '''double émission entière-flottante''' consiste à émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le processeur a donc deux pipelines : un pipeline entier, et un pipeline flottant. Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire, vu que les µops mémoire passent par l'ALU entière avant d’atterrir dans l'unité mémoire. Le reconditionnement de la FPU était utilisé sur les processeurs à double émission entière-flottante. Il entrainait un léger en performance, pour un cout en circuits presque nul.
{|
|[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU.]]
|[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
|}
D'autres processeurs utilisaient uniquement le reconditionnement, mais séparaient l'unité mémoire de l'ALU entière. Ils faisaient donc du '''partitionnement INT-FLOAT-MEM''', avec des contraintes d'appariement relâchées comparé à l'émission entière-flottante. Sur les deux types précédents de CPU, le reconditionnement des unités était souvent utilisé, mais pas systématique. La moitié des processeurs de l'époque l'utilisaient, pas l'autre. Le reconditionnement de l'AGU aurait pu être utilisé, mais ne l'a été que sur l'Intel i960. Ce qui nous amène à parler des autres CPUs.
D'autres CPU superscalaires de l'époque utilisaient la '''double émission entière''', à savoir qu'ils peuvent émettre une opération en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Pour cela, ils peuvent dupliquer une ALU entière, ou reconditionner l'unité mémoire. Les exemples classiques sont les processeurs Intel : i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 reconditionnait l'unité mémoire en ALU entière.
L'Intel i960 utilisait le reconditionnement de l'AGU en plus du partitionnement, ce qui lui permettait d'exécuter une seconde instruction entière, en plus d'une autre instruction. Les instructions en question étaient simples : additions, quelques décalages. De nos jours, ce combo partitionnement et reconditionnement de l'AGU n'est plus utilisé. Il n'a pas beaucoup de sens quand on peut dupliquer des ALU entières, et les processeurs récents ont un budget en transistor suffisant pour.
Le Pentium dupliquait l'ALU entière, mais n'utilisaient aucune forme de partitionnement. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Contrairement au i960, le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
De rares CPUs ont tenté d'utiliser à la fois le partitionnement et la double émission entière. Le résultat utilisait soit la double, soit la triple émission. Les deux CPUs en question sont le PA-RISC PA 7200 d'HP, et le SuperSPARC.
Le PA 7200, sorti en 1994, est un PA-7100 auquel on aurait rajouté une ALU entière. Précisons que ce processeur utilisait aussi le reconditionnement de la FPU, comme son prédécesseur. Les combinaisons autorisées étaient donc INT + INT, INT + MEM, INT + FLOAT/MUL, MEM + FLOAT/MUL. Les combinaisons autorisées par le partitionnement INT-FLOAT-MEM étaient donc possibles, en plus des combinaisons INT + INT, voire INT + INT + MUL (vu que la FPU est reconditionnée). Autrement dit, il pouvait faire toute ce que permettait un CPU à double émission entière, plus les combinaisons FLOAT/MUL + MEM.
Le SuperSPARC était un CPU triple émission, avec deux ALU entières. Il pouvait émettre, à chaque cycle, trois µops en piochant dans deux opérations entières, un accès mémoire, une opération flottante. Le SuperSPARC avait de nombreuses contraintes d’appariement. Par exemple, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Une bonne partie de ces contraintes était lié au banc de registre, qui n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions.
: La seconde ALU a été abandonnée dans l'HyperSPARC, qui est de plus repassé en double émission, ce qui est une évolution à rebours de l'évolution des autres CPUs. Le SuperSPARC était en avance sur son temps, ce quelques années, alors que l'HyperSPARC a un design de son temps.
Le Motorola 88110 était un CPU double émission avec pas moins de 10 unités : deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise.
Pour résumer, voici le tableau chronologique disant quel CPU utilisait quelle technique. Les CPU barrés sont des COU quadruple émission.
* Le partitionnement INT-FLOAT-MEM est en jaune.
* La double émission entière-flottante est en vert.
* La double émission entière est en rouge.
* Les processeurs sans couleur utilisaient soit de la triple émission, soit de la double émission avec un mélange de plusieurs techniques précédentes. Typiquement, ils répartissaient deux µops sur un grand nombre d'unités distinctes.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| class="f_vert | RS/6000 || || || class="f_rouge | i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| class="f_vert | RSC || class="f_vert | PA 7100 || SuperSPARC || || class="f_jaune | 21064 ||
|-
! 1993
| class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || ||
|-
! 1994
| class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 ||
|-
! 1995
| 603/604 || <s>PA 8000</s> || <s>UltraSPARC 1</s> || Microarchitecture P6 (+ concurrence AMD) || || <s>MIPS R1000</s>
|}
C'est la même année , en 1994, que la quadruple émission est arrivée et que les CPU ont commencé à avoir de plus en plus d'unités de calcul. La tendance globale était à la duplication de l'ALU entière, combinée au partitionnement INT-FLOAT-MEM. De quoi exécuter en même temps deux opérations entières, une opération flottante, et un accès mémoire. Quelques CPU, comme les CPU alpha, permettaient d'émettre deux opérations flottantes et/ou deux accès mémoire. Même les designs restés en double émission, avaient ajouté des unités de calcul.
L'alpha 21164 était dans ce cas, tout comme le PA-RISC 7200, le SuperSPARC, et le CPU des PowerPC G3 et G4. 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).
{|class="wikitable"
|+ Processeurs quadruple émission historiques
|-
! Année !! Processeur !! Nombre d'unités
|-
! rowspan="2" | 1994
| POWER 2 || 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'')
|-
| 21164 || 4 unités : 2 ALU + FPU + MEM
|-
! rowspan="3" | 1995
| PA 8000 || 10 unités
|-
| UltraSPARC 1 || 8 unités
|-
| MIPS R1000 || 8 unités
|-
! rowspan="2" | 1996
| 620 || 2 ALU + MUL + FPU + MEM
|-
| 21264 || 4 unités : 2 ALU/MEM + 2 FPU (FADD + FMUL)
|}
La morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes.
Les CPU superscalaires modernes ont des design légèrement différents des CPU historiques. Ils dupliquent l'unité entière, mais leur partitionnement est différent. En général, les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Par exemple, un CPU à 6 voies pourra émettre/exécuter 6 opérations entières simultanément, grâce à 6 ALU entières. Mais il s'agit là d'un maximum et certaines peuvent être remplacées par une opération flottante ou mémoire, qui utilisent l'unité mémoire et/ou la FPU.
Un exemple assez parlant est les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon. C'était une microarchitecture à triple émission, avec trois ALU entières et trois unités mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements.
Il s'agit là d'une tendance, cependant. Les CPU superscalaires tendent à mettre le paquet niveau opérations arithmétiques, mais pas forcément au point de remplir la totalité de la largeur du processeur. La règle est que pour N voies, le processeur dispose d'au moins N/2 ALU entières. Attention, cela ne signifie pas que le processeur émet systématiquement N/2 opération entières. Il peut par exemple émettre une combinaison d'opérations flottantes et mémoire, avec des branchements. Et N/2 est une borne basse, la plupart des CPU x86 ont environ tournent plutôt autour des 2/3, voire des 3/4.
En général, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. La conséquence est que les unités mémoire sont dupliquées, en seconde place dans l'ordre d'importance. Les processeurs x86 ont commencé à dupliquer leurs unités mémoire dès qu'ils ont atteint la quadruple émission, pour les CPU grand public. La microarchitecture Core d'Intel utilisait la quadruple émission, avec deux unités mémoire et trois ALU entières. Les microarchitectures d'AMD K5 et K6 utilisaient aussi la quadruple émission et avaient deux ALUs pour deux unités mémoire.
Plus récemment, la microarchitecture ''Golden Cove'' dispose de cinq unités de calcul trois unités mémoire (deux spécialisées dans les écritures, trois dans les lectures). La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire.Il y a donc soit égalité, soit avantage pour les ALU entières.
Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair.
==L'unité d'émission d'un processeur superscalaire==
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Voyons pourquoi.
===L'excès de ports d'émission===
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ou 5 ?
Une solution est de réduire la largeur du processeur. Dans l'exemple précédent, il est possible de passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La solution précédente est certes très intéressante, mais elle a des défauts. Le principal est que les ports d'émission sont sous-utilisés. Et les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
===L'intérieur de l'unité d'émission===
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
==Le ''front-end'' des processeurs superscalaires==
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Le banc de registre d'un processeur superscalaire===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Une autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions auto-aligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions auto-aligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez longs, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
e59wlvjezo90zuw81x8fb5p1l84x6jm
773120
773119
2026-09-25T15:16:08Z
Mewtow
31375
/* Les CPU superscalaires modernes utilisent toutes ces stratégies */
773120
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
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.
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
===L'évolution historique des processeurs superscalaires===
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. La plupart des CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 sont rapidement montés à de la quadruple émission, avec quelques exceptions. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 ||
|-
! 1995
| 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000
|-
! 1996
| class="f_bleu | 620 || || || || class="f_bleu | 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
==Les unités de calcul d'un processeur superscalaire==
Étudier les CPU superscalaires historique était une bonne introduction propédeutique, nous allons maintenant passer aux CPU superscalaires en général. Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Et cela nous permettra de généraliser certains concepts vus sur les CPU superscalaires historiques.
Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est possible d'exploiter plusieurs unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu.
Les tout premiers CPU superscalaires étaient nombreux à utiliser cette technique. Ils étaient des CPU double émission, avec une ALU entière, une unité mémoire et une FPU. Nous parlerons de '''partitionnement INT-FLOAT-MEM'''. Il s'agit de l'HyperSPARC, de l'alpha 21064 et du PowerPC 603. Les contrainte d’appariement variaient grandement suivant le processeur. Les plus permissifs autorisaient toutes les combinaisons possibles entre : une opération entière, une opération flottante, et une unité mémoire. Mais ils n'étaient pas les seuls.
Le PowerPC 603 implémentait ce genre de double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions).
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Le '''reconditionnement d'une unité fonctionnelle''' permet à celle-ci de faire des opérations qu'elle n'est pas prévue pour. Par exemple, il est possible d'utiliser l'unité mémoire comme seconde ALU entière, ou d'utiliser la FPU comme multiplieur. Ces deux exemples ne sont pas choisit au hasard, nous les étudierons dans ce qui suit. Le reconditionnement évite d'avoir à dupliquer une unité de calcul, tout en ajoutant une voie. Il permet d'améliorer l'efficacité du partitionnement, qu'il soit utilisé seul ou non.
Il est possible de reconditionner la FPU pour faire des multiplications entières. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits. Il est possible d'utiliser de multiplieur pour faire des multiplications entières 32 bits. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
Le reconditionnement de la FPU permet d'exécuter une multiplication entière en plus des autres opérations entières. Par exemple, prenons un CPU avec double émission entière-flottante, sans multiplieur entier séparé. Le reconditionnement de la FPU permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine.
Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants.
Passons maintenant à une seconde forme de reconditionnement. Reconditionner l'unité mémoire permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, des versions améliorées de l'Intel 960. L'Intel 960 originel n'était pas superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire au monde. Il intégrait une ALU entière, une unité mémoire et une unité de branchement. Grâce au partitionnement, il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, en remplacement d'une µop mémoire. Son pipeline permettait donc les combinaisons INT + BRANCH + (MEM / + INT*).
Le cas de l'i960 nous montre que le reconditionnement permet d'améliorer l'intérêt du partitionnement. Elle permet de ne pas dupliquer d'unité de calcul, tout en permettant quand même d'émettre une seconde opération entière.
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''.
Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières ! Il doit y avoir deux ALU entières pour de la double émission entière, trois pour de triple émission éntière, 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]]
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications.
Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que dupliquer l'ALU entière peut se marier parfaitement avec la présence d'une FPU. Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU reconditionne sa FPU ou pas. Mais il est très rare qu'un CPU ait à la fois un multiplieur entier et une FPU reconditionnée.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
Le processeur SuperSPARC avait deux ALU entière et une FPU. La FPU était reconditionnée, ce qui fait que la multiplication était faite dans la FPU, idem pour la division. Il lui est possible d'émettre deux opérations entières simples, en plus d'une multiplication. Par contre, on perd la possibilité d'émettre en même temps une multiplication et une opération flottante. Les combinaisons INT + INT + (MUL/FLOAT) sont possibles pas les combinaisons INT + MUL + FPU.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires modernes utilisent toutes ces stratégies===
Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, duplication de l'ALU entière, duplication des autre unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents.
* Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU.
* Le '''reconditionnement des unités''' a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU.
* La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants.
* La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables.
De nos jours, le partitionnement est utilisé en complément de la duplication des ALU entières, et de la duplication des autres unités. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. Ces modèles utilisaient presque tous la double émission avec trois unités : une ALU, une FPU et une unité mémoire. Si la moitié des processeurs faisait de la double émission "complète", l'autre moitié utilisait une forme encore plus simple de partitionnement.
Pour commencer, étudions le cas des premiers CPU superscalaires à double émission. Ils sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Notamment, la duplication des autres unités ne sert pour ainsi dire à rien, ce qui limite les possibilités, mais rendra les explications plus simples.
La '''double émission entière-flottante''' consiste à émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le processeur a donc deux pipelines : un pipeline entier, et un pipeline flottant. Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire, vu que les µops mémoire passent par l'ALU entière avant d’atterrir dans l'unité mémoire. Le reconditionnement de la FPU était utilisé sur les processeurs à double émission entière-flottante. Il entrainait un léger en performance, pour un cout en circuits presque nul.
{|
|[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU.]]
|[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
|}
D'autres processeurs utilisaient uniquement le reconditionnement, mais séparaient l'unité mémoire de l'ALU entière. Ils faisaient donc du '''partitionnement INT-FLOAT-MEM''', avec des contraintes d'appariement relâchées comparé à l'émission entière-flottante. Sur les deux types précédents de CPU, le reconditionnement des unités était souvent utilisé, mais pas systématique. La moitié des processeurs de l'époque l'utilisaient, pas l'autre. Le reconditionnement de l'AGU aurait pu être utilisé, mais ne l'a été que sur l'Intel i960. Ce qui nous amène à parler des autres CPUs.
D'autres CPU superscalaires de l'époque utilisaient la '''double émission entière''', à savoir qu'ils peuvent émettre une opération en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Pour cela, ils peuvent dupliquer une ALU entière, ou reconditionner l'unité mémoire. Les exemples classiques sont les processeurs Intel : i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 reconditionnait l'unité mémoire en ALU entière.
L'Intel i960 utilisait le reconditionnement de l'AGU en plus du partitionnement, ce qui lui permettait d'exécuter une seconde instruction entière, en plus d'une autre instruction. Les instructions en question étaient simples : additions, quelques décalages. De nos jours, ce combo partitionnement et reconditionnement de l'AGU n'est plus utilisé. Il n'a pas beaucoup de sens quand on peut dupliquer des ALU entières, et les processeurs récents ont un budget en transistor suffisant pour.
Le Pentium dupliquait l'ALU entière, mais n'utilisaient aucune forme de partitionnement. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Contrairement au i960, le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
De rares CPUs ont tenté d'utiliser à la fois le partitionnement et la double émission entière. Le résultat utilisait soit la double, soit la triple émission. Les deux CPUs en question sont le PA-RISC PA 7200 d'HP, et le SuperSPARC.
Le PA 7200, sorti en 1994, est un PA-7100 auquel on aurait rajouté une ALU entière. Précisons que ce processeur utilisait aussi le reconditionnement de la FPU, comme son prédécesseur. Les combinaisons autorisées étaient donc INT + INT, INT + MEM, INT + FLOAT/MUL, MEM + FLOAT/MUL. Les combinaisons autorisées par le partitionnement INT-FLOAT-MEM étaient donc possibles, en plus des combinaisons INT + INT, voire INT + INT + MUL (vu que la FPU est reconditionnée). Autrement dit, il pouvait faire toute ce que permettait un CPU à double émission entière, plus les combinaisons FLOAT/MUL + MEM.
Le SuperSPARC était un CPU triple émission, avec deux ALU entières. Il pouvait émettre, à chaque cycle, trois µops en piochant dans deux opérations entières, un accès mémoire, une opération flottante. Le SuperSPARC avait de nombreuses contraintes d’appariement. Par exemple, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Une bonne partie de ces contraintes était lié au banc de registre, qui n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions.
: La seconde ALU a été abandonnée dans l'HyperSPARC, qui est de plus repassé en double émission, ce qui est une évolution à rebours de l'évolution des autres CPUs. Le SuperSPARC était en avance sur son temps, ce quelques années, alors que l'HyperSPARC a un design de son temps.
Pour résumer, voici le tableau chronologique disant quel CPU utilisait quelle technique. Les CPU barrés sont des COU quadruple émission.
* Le partitionnement INT-FLOAT-MEM est en jaune.
* La double émission entière-flottante est en vert.
* La double émission entière est en rouge.
* Les processeurs sans couleur utilisaient soit de la triple émission, soit de la double émission un peu particulière, on reviendra dessus dans la suite.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| class="f_vert | RS/6000 || || || class="f_rouge | i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| class="f_vert | RSC || class="f_vert | PA 7100 || SuperSPARC || || class="f_jaune | 21064 ||
|-
! 1993
| class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || ||
|-
! 1994
| class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 ||
|-
! 1995
| 603/604 || <s>PA 8000</s> || <s>UltraSPARC 1</s> || Microarchitecture P6 (+ concurrence AMD) || || <s>MIPS R1000</s>
|}
C'est la même année , en 1994, que la quadruple émission est arrivée et que les CPU ont commencé à avoir de plus en plus d'unités de calcul. Même les designs restés en double émission avaient ajouté des unités de calcul. Il y avait alors trois stratégies pour la quadruple émission.
La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Les FPUs étaient soit dupliquées, soit scindées en deux en séparant l'additionneur du multiplieur flottant. Le POWER 2 dupliquait ses FPUs à la perfection, alors que l'21264 partitionnait la FPU. Les accès mémoire étaient traités comme des opérations entières, et le processeur pouvait : soit en faire un seul par cycle, soit en émettre deux, sous conditions (le cache devait être double port pour lire/écrire deux données à la fois, comme on le verra dans la suite du chapitre).
La seconde implémentation dupliquait l'ALU entière et combinait le tout avec un partitionnement INT-FLOAT-MEM. De quoi exécuter en même temps deux opérations entières, une opération flottante, et un accès mémoire. L'alpha 21164 était dans ce cas, tout comme le PA-RISC 7200, le SuperSPARC, et le CPU des PowerPC G3 et G4. 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).
Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, avec au minimum deux ALU entières. Pour comprendre ce que je veux dire, prenons l’exemple du Motorola 88110, sorti en 1991. Il s'agit d'un CPU à double émission avec pas moins de 10 unités : deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise.
La morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes. Le tout est résumé dans le tableau ci-dessous, avec :
* en vert les CPU utilisant la première stratégie ;
* en rouge les CPU utilisant la seconde stratégie ;
* en jaune les CPU utilisant la troisième stratégie.
{|class="wikitable"
|+ Processeurs quadruple émission historiques
|-
! Année !! Processeur !! Unités
|-
! 1991
| class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques
|-
|-
! rowspan="2" | 1994
| class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'')
|-
| class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM
|-
! rowspan="3" | 1995
| class="f_jaune | PA 8000 || class="f_jaune | 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD
|-
| class="f_jaune | UltraSPARC 1 || class="f_jaune | 8 unités
|-
| class="f_jaune | MIPS R1000 || class="f_jaune | 8 unités
|-
! rowspan="2" | 1996
| class="f_rouge | 620 || class="f_rouge | 2 ALU + MUL + FPU + MEM
|-
| class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL)
|}
Les CPU superscalaires modernes ont des design légèrement différents des CPU historiques. Ils dupliquent l'unité entière, mais leur partitionnement est différent. En général, les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Par exemple, un CPU à 6 voies pourra émettre/exécuter 6 opérations entières simultanément, grâce à 6 ALU entières. Mais il s'agit là d'un maximum et certaines peuvent être remplacées par une opération flottante ou mémoire, qui utilisent l'unité mémoire et/ou la FPU.
Un exemple assez parlant est les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon. C'était une microarchitecture à triple émission, avec trois ALU entières et trois unités mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements.
Il s'agit là d'une tendance, cependant. Les CPU superscalaires tendent à mettre le paquet niveau opérations arithmétiques, mais pas forcément au point de remplir la totalité de la largeur du processeur. La règle est que pour N voies, le processeur dispose d'au moins N/2 ALU entières. Attention, cela ne signifie pas que le processeur émet systématiquement N/2 opération entières. Il peut par exemple émettre une combinaison d'opérations flottantes et mémoire, avec des branchements. Et N/2 est une borne basse, la plupart des CPU x86 ont environ tournent plutôt autour des 2/3, voire des 3/4.
En général, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. La conséquence est que les unités mémoire sont dupliquées, en seconde place dans l'ordre d'importance. Les processeurs x86 ont commencé à dupliquer leurs unités mémoire dès qu'ils ont atteint la quadruple émission, pour les CPU grand public. La microarchitecture Core d'Intel utilisait la quadruple émission, avec deux unités mémoire et trois ALU entières. Les microarchitectures d'AMD K5 et K6 utilisaient aussi la quadruple émission et avaient deux ALUs pour deux unités mémoire.
Plus récemment, la microarchitecture ''Golden Cove'' dispose de cinq unités de calcul trois unités mémoire (deux spécialisées dans les écritures, trois dans les lectures). La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire.Il y a donc soit égalité, soit avantage pour les ALU entières.
Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair.
==L'unité d'émission d'un processeur superscalaire==
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Voyons pourquoi.
===L'excès de ports d'émission===
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ou 5 ?
Une solution est de réduire la largeur du processeur. Dans l'exemple précédent, il est possible de passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La solution précédente est certes très intéressante, mais elle a des défauts. Le principal est que les ports d'émission sont sous-utilisés. Et les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
===L'intérieur de l'unité d'émission===
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
==Le ''front-end'' des processeurs superscalaires==
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Le banc de registre d'un processeur superscalaire===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Une autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions auto-aligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions auto-aligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez longs, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
129vsge2yejy8yoi3rs65i8c7g26i7d
773121
773120
2026-09-25T15:17:27Z
Mewtow
31375
/* Les CPU superscalaires modernes utilisent toutes ces stratégies */
773121
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
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.
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
===L'évolution historique des processeurs superscalaires===
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. La plupart des CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 sont rapidement montés à de la quadruple émission, avec quelques exceptions. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 ||
|-
! 1995
| 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000
|-
! 1996
| class="f_bleu | 620 || || || || class="f_bleu | 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
==Les unités de calcul d'un processeur superscalaire==
Étudier les CPU superscalaires historique était une bonne introduction propédeutique, nous allons maintenant passer aux CPU superscalaires en général. Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Et cela nous permettra de généraliser certains concepts vus sur les CPU superscalaires historiques.
Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est possible d'exploiter plusieurs unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu.
Les tout premiers CPU superscalaires étaient nombreux à utiliser cette technique. Ils étaient des CPU double émission, avec une ALU entière, une unité mémoire et une FPU. Nous parlerons de '''partitionnement INT-FLOAT-MEM'''. Il s'agit de l'HyperSPARC, de l'alpha 21064 et du PowerPC 603. Les contrainte d’appariement variaient grandement suivant le processeur. Les plus permissifs autorisaient toutes les combinaisons possibles entre : une opération entière, une opération flottante, et une unité mémoire. Mais ils n'étaient pas les seuls.
Le PowerPC 603 implémentait ce genre de double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions).
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Le '''reconditionnement d'une unité fonctionnelle''' permet à celle-ci de faire des opérations qu'elle n'est pas prévue pour. Par exemple, il est possible d'utiliser l'unité mémoire comme seconde ALU entière, ou d'utiliser la FPU comme multiplieur. Ces deux exemples ne sont pas choisit au hasard, nous les étudierons dans ce qui suit. Le reconditionnement évite d'avoir à dupliquer une unité de calcul, tout en ajoutant une voie. Il permet d'améliorer l'efficacité du partitionnement, qu'il soit utilisé seul ou non.
Il est possible de reconditionner la FPU pour faire des multiplications entières. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits. Il est possible d'utiliser de multiplieur pour faire des multiplications entières 32 bits. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
Le reconditionnement de la FPU permet d'exécuter une multiplication entière en plus des autres opérations entières. Par exemple, prenons un CPU avec double émission entière-flottante, sans multiplieur entier séparé. Le reconditionnement de la FPU permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine.
Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants.
Passons maintenant à une seconde forme de reconditionnement. Reconditionner l'unité mémoire permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, des versions améliorées de l'Intel 960. L'Intel 960 originel n'était pas superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire au monde. Il intégrait une ALU entière, une unité mémoire et une unité de branchement. Grâce au partitionnement, il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, en remplacement d'une µop mémoire. Son pipeline permettait donc les combinaisons INT + BRANCH + (MEM / + INT*).
Le cas de l'i960 nous montre que le reconditionnement permet d'améliorer l'intérêt du partitionnement. Elle permet de ne pas dupliquer d'unité de calcul, tout en permettant quand même d'émettre une seconde opération entière.
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''.
Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières ! Il doit y avoir deux ALU entières pour de la double émission entière, trois pour de triple émission éntière, 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]]
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications.
Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que dupliquer l'ALU entière peut se marier parfaitement avec la présence d'une FPU. Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU reconditionne sa FPU ou pas. Mais il est très rare qu'un CPU ait à la fois un multiplieur entier et une FPU reconditionnée.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
Le processeur SuperSPARC avait deux ALU entière et une FPU. La FPU était reconditionnée, ce qui fait que la multiplication était faite dans la FPU, idem pour la division. Il lui est possible d'émettre deux opérations entières simples, en plus d'une multiplication. Par contre, on perd la possibilité d'émettre en même temps une multiplication et une opération flottante. Les combinaisons INT + INT + (MUL/FLOAT) sont possibles pas les combinaisons INT + MUL + FPU.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires modernes utilisent toutes ces stratégies===
Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, duplication de l'ALU entière, duplication des autre unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents.
* Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU.
* Le '''reconditionnement des unités''' a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU.
* La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants.
* La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables.
De nos jours, le partitionnement est utilisé en complément de la duplication des ALU entières, et de la duplication des autres unités. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. Ces modèles utilisaient presque tous la double émission avec trois unités : une ALU, une FPU et une unité mémoire. Si la moitié des processeurs faisait de la double émission "complète", l'autre moitié utilisait une forme encore plus simple de partitionnement.
Pour commencer, étudions le cas des premiers CPU superscalaires à double émission. Ils sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Notamment, la duplication des autres unités ne sert pour ainsi dire à rien, ce qui limite les possibilités, mais rendra les explications plus simples.
La '''double émission entière-flottante''' consiste à émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le processeur a donc deux pipelines : un pipeline entier, et un pipeline flottant. Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire, vu que les µops mémoire passent par l'ALU entière avant d’atterrir dans l'unité mémoire. Le reconditionnement de la FPU était utilisé sur les processeurs à double émission entière-flottante. Il entrainait un léger en performance, pour un cout en circuits presque nul.
{|
|[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU.]]
|[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
|}
D'autres processeurs utilisaient uniquement le reconditionnement, mais séparaient l'unité mémoire de l'ALU entière. Ils faisaient donc du '''partitionnement INT-FLOAT-MEM''', avec des contraintes d'appariement relâchées comparé à l'émission entière-flottante. Sur les deux types précédents de CPU, le reconditionnement des unités était souvent utilisé, mais pas systématique. La moitié des processeurs de l'époque l'utilisaient, pas l'autre. Le reconditionnement de l'AGU aurait pu être utilisé, mais ne l'a été que sur l'Intel i960. Ce qui nous amène à parler des autres CPUs.
D'autres CPU superscalaires de l'époque utilisaient la '''double émission entière''', à savoir qu'ils peuvent émettre une opération en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Pour cela, ils peuvent dupliquer une ALU entière, ou reconditionner l'unité mémoire. Les exemples classiques sont les processeurs Intel : i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 reconditionnait l'unité mémoire en ALU entière.
L'Intel i960 utilisait le reconditionnement de l'AGU en plus du partitionnement, ce qui lui permettait d'exécuter une seconde instruction entière, en plus d'une autre instruction. Les instructions en question étaient simples : additions, quelques décalages. De nos jours, ce combo partitionnement et reconditionnement de l'AGU n'est plus utilisé. Il n'a pas beaucoup de sens quand on peut dupliquer des ALU entières, et les processeurs récents ont un budget en transistor suffisant pour.
Le Pentium dupliquait l'ALU entière, mais n'utilisaient aucune forme de partitionnement. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Contrairement au i960, le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
De rares CPUs ont tenté d'utiliser à la fois le partitionnement et la double émission entière. Le résultat utilisait soit la double, soit la triple émission. Les deux CPUs en question sont le PA-RISC PA 7200 d'HP, et le SuperSPARC.
Le PA 7200, sorti en 1994, est un PA-7100 auquel on aurait rajouté une ALU entière. Précisons que ce processeur utilisait aussi le reconditionnement de la FPU, comme son prédécesseur. Les combinaisons autorisées étaient donc INT + INT, INT + MEM, INT + FLOAT/MUL, MEM + FLOAT/MUL. Les combinaisons autorisées par le partitionnement INT-FLOAT-MEM étaient donc possibles, en plus des combinaisons INT + INT, voire INT + INT + MUL (vu que la FPU est reconditionnée). Autrement dit, il pouvait faire toute ce que permettait un CPU à double émission entière, plus les combinaisons FLOAT/MUL + MEM.
Le SuperSPARC était un CPU triple émission, avec deux ALU entières. Il pouvait émettre, à chaque cycle, trois µops en piochant dans deux opérations entières, un accès mémoire, une opération flottante. Le SuperSPARC avait de nombreuses contraintes d’appariement. Par exemple, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Une bonne partie de ces contraintes était lié au banc de registre, qui n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions.
: La seconde ALU a été abandonnée dans l'HyperSPARC, qui est de plus repassé en double émission, ce qui est une évolution à rebours de l'évolution des autres CPUs. Le SuperSPARC était en avance sur son temps, ce quelques années, alors que l'HyperSPARC a un design de son temps.
Pour résumer, voici le tableau chronologique disant quel CPU utilisait quelle technique. Les CPU barrés sont des COU quadruple émission.
* Le partitionnement INT-FLOAT-MEM est en jaune.
* La double émission entière-flottante est en vert.
* La double émission entière est en rouge.
* Les processeurs sans couleur utilisaient soit de la triple émission, soit de la double émission un peu particulière, on reviendra dessus dans la suite.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| class="f_vert | RS/6000 || || || class="f_rouge | i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| class="f_vert | RSC || class="f_vert | PA 7100 || SuperSPARC || || class="f_jaune | 21064 ||
|-
! 1993
| class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || ||
|-
! 1994
| class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 ||
|-
! 1995
| 603/604 || <s>PA 8000</s> || <s>UltraSPARC 1</s> || Microarchitecture P6 (+ concurrence AMD) || || <s>MIPS R1000</s>
|}
C'est la même année , en 1994, que la quadruple émission est arrivée et que les CPU ont commencé à avoir de plus en plus d'unités de calcul. Même les designs restés en double émission avaient ajouté des unités de calcul. Il y avait alors trois stratégies pour la quadruple émission.
La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Les FPUs étaient soit dupliquées, soit scindées en deux en séparant l'additionneur du multiplieur flottant. Le POWER 2 dupliquait ses FPUs à la perfection, alors que l'21264 partitionnait la FPU. Les accès mémoire étaient traités comme des opérations entières, et le processeur pouvait : soit en faire un seul par cycle, soit en émettre deux, sous conditions (le cache devait être double port pour lire/écrire deux données à la fois, comme on le verra dans la suite du chapitre).
La seconde implémentation dupliquait l'ALU entière et combinait le tout avec un partitionnement INT-FLOAT-MEM. De quoi exécuter en même temps deux opérations entières, une opération flottante, et un accès mémoire. 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).
Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, avec au minimum deux ALU entières. Pour comprendre ce que je veux dire, prenons l’exemple du Motorola 88110, sorti en 1991. Il s'agit d'un CPU à double émission avec pas moins de 10 unités : deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise.
La morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes. Le tout est résumé dans le tableau ci-dessous, avec :
* en vert les CPU utilisant la première stratégie ;
* en rouge les CPU utilisant la seconde stratégie ;
* en jaune les CPU utilisant la troisième stratégie.
{|class="wikitable"
|+ Processeurs quadruple émission historiques
|-
! Année !! Processeur !! Unités
|-
! 1991
| class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques
|-
|-
! rowspan="2" | 1994
| class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'')
|-
| class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM
|-
! rowspan="3" | 1995
| class="f_jaune | PA 8000 || class="f_jaune | 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD
|-
| class="f_jaune | UltraSPARC 1 || class="f_jaune | 8 unités
|-
| class="f_jaune | MIPS R1000 || class="f_jaune | 8 unités
|-
! rowspan="2" | 1996
| class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM
|-
| class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL)
|}
Les CPU superscalaires modernes ont des design légèrement différents des CPU historiques. Ils dupliquent l'unité entière, mais leur partitionnement est différent. En général, les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Par exemple, un CPU à 6 voies pourra émettre/exécuter 6 opérations entières simultanément, grâce à 6 ALU entières. Mais il s'agit là d'un maximum et certaines peuvent être remplacées par une opération flottante ou mémoire, qui utilisent l'unité mémoire et/ou la FPU.
Un exemple assez parlant est les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon. C'était une microarchitecture à triple émission, avec trois ALU entières et trois unités mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements.
Il s'agit là d'une tendance, cependant. Les CPU superscalaires tendent à mettre le paquet niveau opérations arithmétiques, mais pas forcément au point de remplir la totalité de la largeur du processeur. La règle est que pour N voies, le processeur dispose d'au moins N/2 ALU entières. Attention, cela ne signifie pas que le processeur émet systématiquement N/2 opération entières. Il peut par exemple émettre une combinaison d'opérations flottantes et mémoire, avec des branchements. Et N/2 est une borne basse, la plupart des CPU x86 ont environ tournent plutôt autour des 2/3, voire des 3/4.
En général, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. La conséquence est que les unités mémoire sont dupliquées, en seconde place dans l'ordre d'importance. Les processeurs x86 ont commencé à dupliquer leurs unités mémoire dès qu'ils ont atteint la quadruple émission, pour les CPU grand public. La microarchitecture Core d'Intel utilisait la quadruple émission, avec deux unités mémoire et trois ALU entières. Les microarchitectures d'AMD K5 et K6 utilisaient aussi la quadruple émission et avaient deux ALUs pour deux unités mémoire.
Plus récemment, la microarchitecture ''Golden Cove'' dispose de cinq unités de calcul trois unités mémoire (deux spécialisées dans les écritures, trois dans les lectures). La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire.Il y a donc soit égalité, soit avantage pour les ALU entières.
Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair.
==L'unité d'émission d'un processeur superscalaire==
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Voyons pourquoi.
===L'excès de ports d'émission===
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ou 5 ?
Une solution est de réduire la largeur du processeur. Dans l'exemple précédent, il est possible de passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La solution précédente est certes très intéressante, mais elle a des défauts. Le principal est que les ports d'émission sont sous-utilisés. Et les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
===L'intérieur de l'unité d'émission===
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
==Le ''front-end'' des processeurs superscalaires==
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Le banc de registre d'un processeur superscalaire===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Une autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions auto-aligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions auto-aligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez longs, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
43cxt9zzbicxxrf7utzowyha7xiwlq9
773122
773121
2026-09-25T15:21:26Z
Mewtow
31375
/* Les CPU superscalaires modernes utilisent toutes ces stratégies */
773122
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
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.
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
===L'évolution historique des processeurs superscalaires===
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. La plupart des CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 sont rapidement montés à de la quadruple émission, avec quelques exceptions. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 ||
|-
! 1995
| 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000
|-
! 1996
| class="f_bleu | 620 || || || || class="f_bleu | 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
==Les unités de calcul d'un processeur superscalaire==
Étudier les CPU superscalaires historique était une bonne introduction propédeutique, nous allons maintenant passer aux CPU superscalaires en général. Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Et cela nous permettra de généraliser certains concepts vus sur les CPU superscalaires historiques.
Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est possible d'exploiter plusieurs unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu.
Les tout premiers CPU superscalaires étaient nombreux à utiliser cette technique. Ils étaient des CPU double émission, avec une ALU entière, une unité mémoire et une FPU. Nous parlerons de '''partitionnement INT-FLOAT-MEM'''. Il s'agit de l'HyperSPARC, de l'alpha 21064 et du PowerPC 603. Les contrainte d’appariement variaient grandement suivant le processeur. Les plus permissifs autorisaient toutes les combinaisons possibles entre : une opération entière, une opération flottante, et une unité mémoire. Mais ils n'étaient pas les seuls.
Le PowerPC 603 implémentait ce genre de double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions).
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Le '''reconditionnement d'une unité fonctionnelle''' permet à celle-ci de faire des opérations qu'elle n'est pas prévue pour. Par exemple, il est possible d'utiliser l'unité mémoire comme seconde ALU entière, ou d'utiliser la FPU comme multiplieur. Ces deux exemples ne sont pas choisit au hasard, nous les étudierons dans ce qui suit. Le reconditionnement évite d'avoir à dupliquer une unité de calcul, tout en ajoutant une voie. Il permet d'améliorer l'efficacité du partitionnement, qu'il soit utilisé seul ou non.
Il est possible de reconditionner la FPU pour faire des multiplications entières. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits. Il est possible d'utiliser de multiplieur pour faire des multiplications entières 32 bits. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
Le reconditionnement de la FPU permet d'exécuter une multiplication entière en plus des autres opérations entières. Par exemple, prenons un CPU avec double émission entière-flottante, sans multiplieur entier séparé. Le reconditionnement de la FPU permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine.
Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants.
Passons maintenant à une seconde forme de reconditionnement. Reconditionner l'unité mémoire permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, des versions améliorées de l'Intel 960. L'Intel 960 originel n'était pas superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire au monde. Il intégrait une ALU entière, une unité mémoire et une unité de branchement. Grâce au partitionnement, il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, en remplacement d'une µop mémoire. Son pipeline permettait donc les combinaisons INT + BRANCH + (MEM / + INT*).
Le cas de l'i960 nous montre que le reconditionnement permet d'améliorer l'intérêt du partitionnement. Elle permet de ne pas dupliquer d'unité de calcul, tout en permettant quand même d'émettre une seconde opération entière.
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''.
Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières ! Il doit y avoir deux ALU entières pour de la double émission entière, trois pour de triple émission éntière, 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]]
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications.
Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que dupliquer l'ALU entière peut se marier parfaitement avec la présence d'une FPU. Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU reconditionne sa FPU ou pas. Mais il est très rare qu'un CPU ait à la fois un multiplieur entier et une FPU reconditionnée.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
Le processeur SuperSPARC avait deux ALU entière et une FPU. La FPU était reconditionnée, ce qui fait que la multiplication était faite dans la FPU, idem pour la division. Il lui est possible d'émettre deux opérations entières simples, en plus d'une multiplication. Par contre, on perd la possibilité d'émettre en même temps une multiplication et une opération flottante. Les combinaisons INT + INT + (MUL/FLOAT) sont possibles pas les combinaisons INT + MUL + FPU.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires modernes utilisent toutes ces stratégies===
Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, duplication de l'ALU entière, duplication des autre unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents.
* Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU.
* Le '''reconditionnement des unités''' a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU.
* La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants.
* La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables.
De nos jours, le partitionnement est utilisé en complément de la duplication des ALU entières, et de la duplication des autres unités. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. Ces modèles utilisaient presque tous la double émission avec trois unités : une ALU, une FPU et une unité mémoire. Si la moitié des processeurs faisait de la double émission "complète", l'autre moitié utilisait une forme encore plus simple de partitionnement.
Pour commencer, étudions le cas des premiers CPU superscalaires à double émission. Ils sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Notamment, la duplication des autres unités ne sert pour ainsi dire à rien, ce qui limite les possibilités, mais rendra les explications plus simples.
La '''double émission entière-flottante''' consiste à émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le processeur a donc deux pipelines : un pipeline entier, et un pipeline flottant. Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire, vu que les µops mémoire passent par l'ALU entière avant d’atterrir dans l'unité mémoire. Le reconditionnement de la FPU était utilisé sur les processeurs à double émission entière-flottante. Il entrainait un léger en performance, pour un cout en circuits presque nul.
{|
|[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU.]]
|[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
|}
D'autres processeurs utilisaient uniquement le reconditionnement, mais séparaient l'unité mémoire de l'ALU entière. Ils faisaient donc du '''partitionnement INT-FLOAT-MEM''', avec des contraintes d'appariement relâchées comparé à l'émission entière-flottante. Sur les deux types précédents de CPU, le reconditionnement des unités était souvent utilisé, mais pas systématique. La moitié des processeurs de l'époque l'utilisaient, pas l'autre. Le reconditionnement de l'AGU aurait pu être utilisé, mais ne l'a été que sur l'Intel i960. Ce qui nous amène à parler des autres CPUs.
D'autres CPU superscalaires de l'époque utilisaient la '''double émission entière''', à savoir qu'ils peuvent émettre une opération en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Pour cela, ils peuvent dupliquer une ALU entière, ou reconditionner l'unité mémoire. Les exemples classiques sont les processeurs Intel : i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 reconditionnait l'unité mémoire en ALU entière.
L'Intel i960 utilisait le reconditionnement de l'AGU en plus du partitionnement, ce qui lui permettait d'exécuter une seconde instruction entière, en plus d'une autre instruction. Les instructions en question étaient simples : additions, quelques décalages. De nos jours, ce combo partitionnement et reconditionnement de l'AGU n'est plus utilisé. Il n'a pas beaucoup de sens quand on peut dupliquer des ALU entières, et les processeurs récents ont un budget en transistor suffisant pour.
Le Pentium dupliquait l'ALU entière, mais n'utilisaient aucune forme de partitionnement. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Contrairement au i960, le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
De rares CPUs ont tenté d'utiliser à la fois le partitionnement et la double émission entière. Le résultat utilisait soit la double, soit la triple émission. Les deux CPUs en question sont le PA-RISC PA 7200 d'HP, et le SuperSPARC.
Le PA 7200, sorti en 1994, est un PA-7100 auquel on aurait rajouté une ALU entière. Précisons que ce processeur utilisait aussi le reconditionnement de la FPU, comme son prédécesseur. Les combinaisons autorisées étaient donc INT + INT, INT + MEM, INT + FLOAT/MUL, MEM + FLOAT/MUL. Les combinaisons autorisées par le partitionnement INT-FLOAT-MEM étaient donc possibles, en plus des combinaisons INT + INT, voire INT + INT + MUL (vu que la FPU est reconditionnée). Autrement dit, il pouvait faire toute ce que permettait un CPU à double émission entière, plus les combinaisons FLOAT/MUL + MEM.
Le SuperSPARC était un CPU triple émission, avec deux ALU entières. Il pouvait émettre, à chaque cycle, trois µops en piochant dans deux opérations entières, un accès mémoire, une opération flottante. Le SuperSPARC avait de nombreuses contraintes d’appariement. Par exemple, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Une bonne partie de ces contraintes était lié au banc de registre, qui n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions.
: La seconde ALU a été abandonnée dans l'HyperSPARC, qui est de plus repassé en double émission, ce qui est une évolution à rebours de l'évolution des autres CPUs. Le SuperSPARC était en avance sur son temps, ce quelques années, alors que l'HyperSPARC a un design de son temps.
Pour résumer, voici le tableau chronologique disant quel CPU utilisait quelle technique. Les CPU barrés sont des COU quadruple émission.
* Le partitionnement INT-FLOAT-MEM est en jaune.
* La double émission entière-flottante est en vert.
* La double émission entière est en rouge.
* Les processeurs sans couleur utilisaient soit de la triple émission, soit de la double émission un peu particulière, on reviendra dessus dans la suite.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| class="f_vert | RS/6000 || || || class="f_rouge | i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| class="f_vert | RSC || class="f_vert | PA 7100 || SuperSPARC || || class="f_jaune | 21064 ||
|-
! 1993
| class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || ||
|-
! 1994
| class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 ||
|-
! 1995
| 603/604 || <s>PA 8000</s> || <s>UltraSPARC 1</s> || Microarchitecture P6 (+ concurrence AMD) || || <s>MIPS R1000</s>
|}
C'est la même année , en 1994, que la quadruple émission est arrivée et que les CPU ont commencé à avoir de plus en plus d'unités de calcul. Même les designs restés en double émission avaient ajouté des unités de calcul. Il y avait alors trois stratégies pour la quadruple émission.
La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Les FPUs étaient soit dupliquées, soit scindées en deux en séparant l'additionneur du multiplieur flottant. Le POWER 2 dupliquait ses FPUs à la perfection, alors que l'21264 partitionnait la FPU. Les accès mémoire étaient traités comme des opérations entières, et le processeur pouvait : soit en faire un seul par cycle, soit en émettre deux, sous conditions (le cache devait être double port pour lire/écrire deux données à la fois, comme on le verra dans la suite du chapitre).
La seconde implémentation dupliquait l'ALU entière et combinait le tout avec un partitionnement INT-FLOAT-MEM. De quoi exécuter en même temps deux opérations entières, une opération flottante, et un accès mémoire. 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).
Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, avec au minimum deux ALU entières. Pour comprendre ce que je veux dire, prenons l'exemple de l'UltraSPARC. Il avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplier flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes. Le processeur pouvait émettre 4 instructions, tant qu'elles atterrissaient dans des unités différentes.
La même méthode peut être utilisée pour la double émission. Le Motorola 88110, sorti en 1991, était un CPU à double émission avec pas moins de 10 unités : deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise.
La morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes. Le tout est résumé dans le tableau ci-dessous, avec :
* en vert les CPU utilisant la première stratégie ;
* en rouge les CPU utilisant la seconde stratégie ;
* en jaune les CPU utilisant la troisième stratégie.
{|class="wikitable"
|+ Processeurs quadruple émission historiques
|-
! Année !! Processeur !! Unités
|-
! 1991
| class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques
|-
|-
! rowspan="2" | 1994
| class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'')
|-
| class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM
|-
! rowspan="3" | 1995
| class="f_jaune | PA 8000 || class="f_jaune | 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD
|-
| class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques
|-
| class="f_jaune | MIPS R1000 || class="f_jaune | 8 unités
|-
! rowspan="2" | 1996
| class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM
|-
| class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL)
|}
Les CPU superscalaires modernes ont des design légèrement différents des CPU historiques. Ils dupliquent l'unité entière, mais leur partitionnement est différent. En général, les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Par exemple, un CPU à 6 voies pourra émettre/exécuter 6 opérations entières simultanément, grâce à 6 ALU entières. Mais il s'agit là d'un maximum et certaines peuvent être remplacées par une opération flottante ou mémoire, qui utilisent l'unité mémoire et/ou la FPU.
Un exemple assez parlant est les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon. C'était une microarchitecture à triple émission, avec trois ALU entières et trois unités mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements.
Il s'agit là d'une tendance, cependant. Les CPU superscalaires tendent à mettre le paquet niveau opérations arithmétiques, mais pas forcément au point de remplir la totalité de la largeur du processeur. La règle est que pour N voies, le processeur dispose d'au moins N/2 ALU entières. Attention, cela ne signifie pas que le processeur émet systématiquement N/2 opération entières. Il peut par exemple émettre une combinaison d'opérations flottantes et mémoire, avec des branchements. Et N/2 est une borne basse, la plupart des CPU x86 ont environ tournent plutôt autour des 2/3, voire des 3/4.
En général, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. La conséquence est que les unités mémoire sont dupliquées, en seconde place dans l'ordre d'importance. Les processeurs x86 ont commencé à dupliquer leurs unités mémoire dès qu'ils ont atteint la quadruple émission, pour les CPU grand public. La microarchitecture Core d'Intel utilisait la quadruple émission, avec deux unités mémoire et trois ALU entières. Les microarchitectures d'AMD K5 et K6 utilisaient aussi la quadruple émission et avaient deux ALUs pour deux unités mémoire.
Plus récemment, la microarchitecture ''Golden Cove'' dispose de cinq unités de calcul trois unités mémoire (deux spécialisées dans les écritures, trois dans les lectures). La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire.Il y a donc soit égalité, soit avantage pour les ALU entières.
Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair.
==L'unité d'émission d'un processeur superscalaire==
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Voyons pourquoi.
===L'excès de ports d'émission===
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ou 5 ?
Une solution est de réduire la largeur du processeur. Dans l'exemple précédent, il est possible de passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La solution précédente est certes très intéressante, mais elle a des défauts. Le principal est que les ports d'émission sont sous-utilisés. Et les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
===L'intérieur de l'unité d'émission===
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
==Le ''front-end'' des processeurs superscalaires==
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Le banc de registre d'un processeur superscalaire===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Une autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions auto-aligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions auto-aligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez longs, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
jfyilh1rd8kl97qyg4snhlst1latv2j
773123
773122
2026-09-25T15:31:41Z
Mewtow
31375
/* Les CPU superscalaires modernes utilisent toutes ces stratégies */
773123
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
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.
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
===L'évolution historique des processeurs superscalaires===
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. La plupart des CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 sont rapidement montés à de la quadruple émission, avec quelques exceptions. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 ||
|-
! 1995
| 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000
|-
! 1996
| class="f_bleu | 620 || || || || class="f_bleu | 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
==Les unités de calcul d'un processeur superscalaire==
Étudier les CPU superscalaires historique était une bonne introduction propédeutique, nous allons maintenant passer aux CPU superscalaires en général. Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Et cela nous permettra de généraliser certains concepts vus sur les CPU superscalaires historiques.
Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est possible d'exploiter plusieurs unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu.
Les tout premiers CPU superscalaires étaient nombreux à utiliser cette technique. Ils étaient des CPU double émission, avec une ALU entière, une unité mémoire et une FPU. Nous parlerons de '''partitionnement INT-FLOAT-MEM'''. Il s'agit de l'HyperSPARC, de l'alpha 21064 et du PowerPC 603. Les contrainte d’appariement variaient grandement suivant le processeur. Les plus permissifs autorisaient toutes les combinaisons possibles entre : une opération entière, une opération flottante, et une unité mémoire. Mais ils n'étaient pas les seuls.
Le PowerPC 603 implémentait ce genre de double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions).
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Le '''reconditionnement d'une unité fonctionnelle''' permet à celle-ci de faire des opérations qu'elle n'est pas prévue pour. Par exemple, il est possible d'utiliser l'unité mémoire comme seconde ALU entière, ou d'utiliser la FPU comme multiplieur. Ces deux exemples ne sont pas choisit au hasard, nous les étudierons dans ce qui suit. Le reconditionnement évite d'avoir à dupliquer une unité de calcul, tout en ajoutant une voie. Il permet d'améliorer l'efficacité du partitionnement, qu'il soit utilisé seul ou non.
Il est possible de reconditionner la FPU pour faire des multiplications entières. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits. Il est possible d'utiliser de multiplieur pour faire des multiplications entières 32 bits. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
Le reconditionnement de la FPU permet d'exécuter une multiplication entière en plus des autres opérations entières. Par exemple, prenons un CPU avec double émission entière-flottante, sans multiplieur entier séparé. Le reconditionnement de la FPU permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine.
Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants.
Passons maintenant à une seconde forme de reconditionnement. Reconditionner l'unité mémoire permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, des versions améliorées de l'Intel 960. L'Intel 960 originel n'était pas superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire au monde. Il intégrait une ALU entière, une unité mémoire et une unité de branchement. Grâce au partitionnement, il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, en remplacement d'une µop mémoire. Son pipeline permettait donc les combinaisons INT + BRANCH + (MEM / + INT*).
Le cas de l'i960 nous montre que le reconditionnement permet d'améliorer l'intérêt du partitionnement. Elle permet de ne pas dupliquer d'unité de calcul, tout en permettant quand même d'émettre une seconde opération entière.
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''.
Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières ! Il doit y avoir deux ALU entières pour de la double émission entière, trois pour de triple émission éntière, 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]]
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications.
Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que dupliquer l'ALU entière peut se marier parfaitement avec la présence d'une FPU. Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU reconditionne sa FPU ou pas. Mais il est très rare qu'un CPU ait à la fois un multiplieur entier et une FPU reconditionnée.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
Le processeur SuperSPARC avait deux ALU entière et une FPU. La FPU était reconditionnée, ce qui fait que la multiplication était faite dans la FPU, idem pour la division. Il lui est possible d'émettre deux opérations entières simples, en plus d'une multiplication. Par contre, on perd la possibilité d'émettre en même temps une multiplication et une opération flottante. Les combinaisons INT + INT + (MUL/FLOAT) sont possibles pas les combinaisons INT + MUL + FPU.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires modernes utilisent toutes ces stratégies===
Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, duplication de l'ALU entière, duplication des autre unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents.
* Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU.
* Le '''reconditionnement des unités''' a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU.
* La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants.
* La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables.
De nos jours, le partitionnement est utilisé en complément de la duplication des ALU entières, et de la duplication des autres unités. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. Ces modèles utilisaient presque tous la double émission avec trois unités : une ALU, une FPU et une unité mémoire. Si la moitié des processeurs faisait de la double émission "complète", l'autre moitié utilisait une forme encore plus simple de partitionnement.
Pour commencer, étudions le cas des premiers CPU superscalaires à double émission. Ils sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Notamment, la duplication des autres unités ne sert pour ainsi dire à rien, ce qui limite les possibilités, mais rendra les explications plus simples.
La '''double émission entière-flottante''' consiste à émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le processeur a donc deux pipelines : un pipeline entier, et un pipeline flottant. Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire, vu que les µops mémoire passent par l'ALU entière avant d’atterrir dans l'unité mémoire. Le reconditionnement de la FPU était utilisé sur les processeurs à double émission entière-flottante. Il entrainait un léger en performance, pour un cout en circuits presque nul.
{|
|[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU.]]
|[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
|}
D'autres processeurs utilisaient uniquement le reconditionnement, mais séparaient l'unité mémoire de l'ALU entière. Ils faisaient donc du '''partitionnement INT-FLOAT-MEM''', avec des contraintes d'appariement relâchées comparé à l'émission entière-flottante. Sur les deux types précédents de CPU, le reconditionnement des unités était souvent utilisé, mais pas systématique. La moitié des processeurs de l'époque l'utilisaient, pas l'autre. Le reconditionnement de l'AGU aurait pu être utilisé, mais ne l'a été que sur l'Intel i960. Ce qui nous amène à parler des autres CPUs.
D'autres CPU superscalaires de l'époque utilisaient la '''double émission entière''', à savoir qu'ils peuvent émettre une opération en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Pour cela, ils peuvent dupliquer une ALU entière, ou reconditionner l'unité mémoire. Les exemples classiques sont les processeurs Intel : i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 reconditionnait l'unité mémoire en ALU entière.
L'Intel i960 utilisait le reconditionnement de l'AGU en plus du partitionnement, ce qui lui permettait d'exécuter une seconde instruction entière, en plus d'une autre instruction. Les instructions en question étaient simples : additions, quelques décalages. De nos jours, ce combo partitionnement et reconditionnement de l'AGU n'est plus utilisé. Il n'a pas beaucoup de sens quand on peut dupliquer des ALU entières, et les processeurs récents ont un budget en transistor suffisant pour.
Le Pentium dupliquait l'ALU entière, mais n'utilisaient aucune forme de partitionnement. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Contrairement au i960, le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
De rares CPUs ont tenté d'utiliser à la fois le partitionnement et la double émission entière. Le résultat utilisait soit la double, soit la triple émission. Les deux CPUs en question sont le PA-RISC PA 7200 d'HP, et le SuperSPARC.
Le PA 7200, sorti en 1994, est un PA-7100 auquel on aurait rajouté une ALU entière. Précisons que ce processeur utilisait aussi le reconditionnement de la FPU, comme son prédécesseur. Les combinaisons autorisées étaient donc INT + INT, INT + MEM, INT + FLOAT/MUL, MEM + FLOAT/MUL. Les combinaisons autorisées par le partitionnement INT-FLOAT-MEM étaient donc possibles, en plus des combinaisons INT + INT, voire INT + INT + MUL (vu que la FPU est reconditionnée). Autrement dit, il pouvait faire toute ce que permettait un CPU à double émission entière, plus les combinaisons FLOAT/MUL + MEM.
Le SuperSPARC était un CPU triple émission, avec deux ALU entières. Il pouvait émettre, à chaque cycle, trois µops en piochant dans deux opérations entières, un accès mémoire, une opération flottante. Le SuperSPARC avait de nombreuses contraintes d’appariement. Par exemple, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Une bonne partie de ces contraintes était lié au banc de registre, qui n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions.
: La seconde ALU a été abandonnée dans l'HyperSPARC, qui est de plus repassé en double émission, ce qui est une évolution à rebours de l'évolution des autres CPUs. Le SuperSPARC était en avance sur son temps, ce quelques années, alors que l'HyperSPARC a un design de son temps.
Pour résumer, voici le tableau chronologique disant quel CPU utilisait quelle technique. Les CPU barrés sont des COU quadruple émission.
* Le partitionnement INT-FLOAT-MEM est en jaune.
* La double émission entière-flottante est en vert.
* La double émission entière est en rouge.
* Les processeurs sans couleur utilisaient soit de la triple émission, soit de la double émission un peu particulière, on reviendra dessus dans la suite.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| class="f_vert | RS/6000 || || || class="f_rouge | i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| class="f_vert | RSC || class="f_vert | PA 7100 || SuperSPARC || || class="f_jaune | 21064 ||
|-
! 1993
| class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || ||
|-
! 1994
| class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 ||
|-
! 1995
| 603/604 || <s>PA 8000</s> || <s>UltraSPARC 1</s> || Microarchitecture P6 (+ concurrence AMD) || || <s>MIPS R1000</s>
|}
C'est la même année , en 1994, que la quadruple émission est arrivée et que les CPU ont commencé à avoir de plus en plus d'unités de calcul. Même les designs restés en double émission avaient ajouté des unités de calcul. Il y avait alors trois stratégies pour la quadruple émission.
La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Les FPUs étaient soit dupliquées, soit scindées en deux en séparant l'additionneur du multiplieur flottant. Le POWER 2 dupliquait ses FPUs à la perfection, alors que l'21264 partitionnait la FPU. Les accès mémoire étaient traités comme des opérations entières, et le processeur pouvait : soit en faire un seul par cycle, soit en émettre deux, sous conditions (le cache devait être double port pour lire/écrire deux données à la fois, comme on le verra dans la suite du chapitre).
Les processeurs MIPS R8000 et R10000 allaient un peu plus loin, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités.
La seconde implémentation dupliquait l'ALU entière et combinait le tout avec un partitionnement INT-FLOAT-MEM. De quoi exécuter en même temps deux opérations entières, une opération flottante, et un accès mémoire. 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).
Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, avec au minimum deux ALU entières. Pour comprendre ce que je veux dire, prenons l'exemple de l'UltraSPARC. Il avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplier flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes. Le processeur pouvait émettre 4 instructions, tant qu'elles atterrissaient dans des unités différentes.
La même méthode peut être utilisée pour la double émission. Le Motorola 88110, sorti en 1991, était un CPU à double émission avec pas moins de 10 unités : deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise.
La morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes. Le tout est résumé dans le tableau ci-dessous, avec :
* en vert les CPU utilisant la première stratégie ;
* en rouge les CPU utilisant la seconde stratégie ;
* en jaune les CPU utilisant la troisième stratégie.
{|class="wikitable"
|+ Processeurs quadruple émission historiques
|-
! Année !! Processeur !! Unités
|-
! 1991
| class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques
|-
|-
! rowspan="2" | 1994
| class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'')
|-
| class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM
|-
! rowspan="3" | 1995
| class="f_jaune | PA 8000 || class="f_jaune | 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD
|-
| class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques
|-
| MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM
|-
! rowspan="2" | 1996
| class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM
|-
| class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL)
|}
Les CPU superscalaires modernes ont des design légèrement différents des CPU historiques. Ils dupliquent l'unité entière, mais leur partitionnement est différent. En général, les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Par exemple, un CPU à 6 voies pourra émettre/exécuter 6 opérations entières simultanément, grâce à 6 ALU entières. Mais il s'agit là d'un maximum et certaines peuvent être remplacées par une opération flottante ou mémoire, qui utilisent l'unité mémoire et/ou la FPU.
Un exemple assez parlant est les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon. C'était une microarchitecture à triple émission, avec trois ALU entières et trois unités mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements.
Il s'agit là d'une tendance, cependant. Les CPU superscalaires tendent à mettre le paquet niveau opérations arithmétiques, mais pas forcément au point de remplir la totalité de la largeur du processeur. La règle est que pour N voies, le processeur dispose d'au moins N/2 ALU entières. Attention, cela ne signifie pas que le processeur émet systématiquement N/2 opération entières. Il peut par exemple émettre une combinaison d'opérations flottantes et mémoire, avec des branchements. Et N/2 est une borne basse, la plupart des CPU x86 ont environ tournent plutôt autour des 2/3, voire des 3/4.
En général, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. La conséquence est que les unités mémoire sont dupliquées, en seconde place dans l'ordre d'importance. Les processeurs x86 ont commencé à dupliquer leurs unités mémoire dès qu'ils ont atteint la quadruple émission, pour les CPU grand public. La microarchitecture Core d'Intel utilisait la quadruple émission, avec deux unités mémoire et trois ALU entières. Les microarchitectures d'AMD K5 et K6 utilisaient aussi la quadruple émission et avaient deux ALUs pour deux unités mémoire.
Plus récemment, la microarchitecture ''Golden Cove'' dispose de cinq unités de calcul trois unités mémoire (deux spécialisées dans les écritures, trois dans les lectures). La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire.Il y a donc soit égalité, soit avantage pour les ALU entières.
Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair.
==L'unité d'émission d'un processeur superscalaire==
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Voyons pourquoi.
===L'excès de ports d'émission===
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ou 5 ?
Une solution est de réduire la largeur du processeur. Dans l'exemple précédent, il est possible de passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La solution précédente est certes très intéressante, mais elle a des défauts. Le principal est que les ports d'émission sont sous-utilisés. Et les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
===L'intérieur de l'unité d'émission===
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
==Le ''front-end'' des processeurs superscalaires==
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Le banc de registre d'un processeur superscalaire===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Une autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions auto-aligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions auto-aligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez longs, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
lnriiilms59kh745aaopg806ii2k568
773124
773123
2026-09-25T15:37:08Z
Mewtow
31375
/* Les CPU superscalaires modernes utilisent toutes ces stratégies */
773124
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
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.
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
===L'évolution historique des processeurs superscalaires===
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. La plupart des CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 sont rapidement montés à de la quadruple émission, avec quelques exceptions. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 ||
|-
! 1995
| 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000
|-
! 1996
| class="f_bleu | 620 || || || || class="f_bleu | 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
==Les unités de calcul d'un processeur superscalaire==
Étudier les CPU superscalaires historique était une bonne introduction propédeutique, nous allons maintenant passer aux CPU superscalaires en général. Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Et cela nous permettra de généraliser certains concepts vus sur les CPU superscalaires historiques.
Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est possible d'exploiter plusieurs unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu.
Les tout premiers CPU superscalaires étaient nombreux à utiliser cette technique. Ils étaient des CPU double émission, avec une ALU entière, une unité mémoire et une FPU. Nous parlerons de '''partitionnement INT-FLOAT-MEM'''. Il s'agit de l'HyperSPARC, de l'alpha 21064 et du PowerPC 603. Les contrainte d’appariement variaient grandement suivant le processeur. Les plus permissifs autorisaient toutes les combinaisons possibles entre : une opération entière, une opération flottante, et une unité mémoire. Mais ils n'étaient pas les seuls.
Le PowerPC 603 implémentait ce genre de double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions).
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Le '''reconditionnement d'une unité fonctionnelle''' permet à celle-ci de faire des opérations qu'elle n'est pas prévue pour. Par exemple, il est possible d'utiliser l'unité mémoire comme seconde ALU entière, ou d'utiliser la FPU comme multiplieur. Ces deux exemples ne sont pas choisit au hasard, nous les étudierons dans ce qui suit. Le reconditionnement évite d'avoir à dupliquer une unité de calcul, tout en ajoutant une voie. Il permet d'améliorer l'efficacité du partitionnement, qu'il soit utilisé seul ou non.
Il est possible de reconditionner la FPU pour faire des multiplications entières. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits. Il est possible d'utiliser de multiplieur pour faire des multiplications entières 32 bits. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
Le reconditionnement de la FPU permet d'exécuter une multiplication entière en plus des autres opérations entières. Par exemple, prenons un CPU avec double émission entière-flottante, sans multiplieur entier séparé. Le reconditionnement de la FPU permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine.
Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants.
Passons maintenant à une seconde forme de reconditionnement. Reconditionner l'unité mémoire permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, des versions améliorées de l'Intel 960. L'Intel 960 originel n'était pas superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire au monde. Il intégrait une ALU entière, une unité mémoire et une unité de branchement. Grâce au partitionnement, il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, en remplacement d'une µop mémoire. Son pipeline permettait donc les combinaisons INT + BRANCH + (MEM / + INT*).
Le cas de l'i960 nous montre que le reconditionnement permet d'améliorer l'intérêt du partitionnement. Elle permet de ne pas dupliquer d'unité de calcul, tout en permettant quand même d'émettre une seconde opération entière.
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''.
Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières ! Il doit y avoir deux ALU entières pour de la double émission entière, trois pour de triple émission éntière, 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]]
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications.
Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que dupliquer l'ALU entière peut se marier parfaitement avec la présence d'une FPU. Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU reconditionne sa FPU ou pas. Mais il est très rare qu'un CPU ait à la fois un multiplieur entier et une FPU reconditionnée.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
Le processeur SuperSPARC avait deux ALU entière et une FPU. La FPU était reconditionnée, ce qui fait que la multiplication était faite dans la FPU, idem pour la division. Il lui est possible d'émettre deux opérations entières simples, en plus d'une multiplication. Par contre, on perd la possibilité d'émettre en même temps une multiplication et une opération flottante. Les combinaisons INT + INT + (MUL/FLOAT) sont possibles pas les combinaisons INT + MUL + FPU.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires modernes utilisent toutes ces stratégies===
Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, duplication de l'ALU entière, duplication des autre unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents.
* Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU.
* Le '''reconditionnement des unités''' a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU.
* La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants.
* La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables.
De nos jours, le partitionnement est utilisé en complément de la duplication des ALU entières, et de la duplication des autres unités. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993.
Le meilleur moyen de voir comment sont combinés ces quatre technique est de regarder ce qu'il s'est passé historiquement.
==L'évolution historique de la superscalarité==
Pour commencer, étudions le cas des premiers CPU superscalaires à double émission. Ils sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Notamment, la duplication des autres unités ne sert pour ainsi dire à rien, ce qui limite les possibilités, mais rendra les explications plus simples.
Ces modèles utilisaient presque tous la double émission avec trois unités : une ALU, une FPU et une unité mémoire. Si la moitié des processeurs faisait de la double émission "complète", l'autre moitié utilisait une forme encore plus simple de partitionnement.
La '''double émission entière-flottante''' consiste à émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le processeur a donc deux pipelines : un pipeline entier, et un pipeline flottant. Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire, vu que les µops mémoire passent par l'ALU entière avant d’atterrir dans l'unité mémoire. Le reconditionnement de la FPU était utilisé sur les processeurs à double émission entière-flottante. Il entrainait un léger en performance, pour un cout en circuits presque nul.
{|
|[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU.]]
|[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
|}
D'autres processeurs utilisaient uniquement le reconditionnement, mais séparaient l'unité mémoire de l'ALU entière. Ils faisaient donc du '''partitionnement INT-FLOAT-MEM''', avec des contraintes d'appariement relâchées comparé à l'émission entière-flottante. Sur les deux types précédents de CPU, le reconditionnement des unités était souvent utilisé, mais pas systématique. La moitié des processeurs de l'époque l'utilisaient, pas l'autre. Le reconditionnement de l'AGU aurait pu être utilisé, mais ne l'a été que sur l'Intel i960. Ce qui nous amène à parler des autres CPUs.
D'autres CPU superscalaires de l'époque utilisaient la '''double émission entière''', à savoir qu'ils peuvent émettre une opération en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Pour cela, ils peuvent dupliquer une ALU entière, ou reconditionner l'unité mémoire. Les exemples classiques sont les processeurs Intel : i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 reconditionnait l'unité mémoire en ALU entière.
L'Intel i960 utilisait le reconditionnement de l'AGU en plus du partitionnement, ce qui lui permettait d'exécuter une seconde instruction entière, en plus d'une autre instruction. Les instructions en question étaient simples : additions, quelques décalages. De nos jours, ce combo partitionnement et reconditionnement de l'AGU n'est plus utilisé. Il n'a pas beaucoup de sens quand on peut dupliquer des ALU entières, et les processeurs récents ont un budget en transistor suffisant pour.
Le Pentium dupliquait l'ALU entière, mais n'utilisaient aucune forme de partitionnement. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Contrairement au i960, le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
De rares CPUs ont tenté d'utiliser à la fois le partitionnement et la double émission entière. Le résultat utilisait soit la double, soit la triple émission. Les deux CPUs en question sont le PA-RISC PA 7200 d'HP, et le SuperSPARC.
Le PA 7200, sorti en 1994, est un PA-7100 auquel on aurait rajouté une ALU entière. Précisons que ce processeur utilisait aussi le reconditionnement de la FPU, comme son prédécesseur. Les combinaisons autorisées étaient donc INT + INT, INT + MEM, INT + FLOAT/MUL, MEM + FLOAT/MUL. Les combinaisons autorisées par le partitionnement INT-FLOAT-MEM étaient donc possibles, en plus des combinaisons INT + INT, voire INT + INT + MUL (vu que la FPU est reconditionnée). Autrement dit, il pouvait faire toute ce que permettait un CPU à double émission entière, plus les combinaisons FLOAT/MUL + MEM.
Le SuperSPARC était un CPU triple émission, avec deux ALU entières. Il pouvait émettre, à chaque cycle, trois µops en piochant dans deux opérations entières, un accès mémoire, une opération flottante. Le SuperSPARC avait de nombreuses contraintes d’appariement. Par exemple, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Une bonne partie de ces contraintes était lié au banc de registre, qui n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions.
: La seconde ALU a été abandonnée dans l'HyperSPARC, qui est de plus repassé en double émission, ce qui est une évolution à rebours de l'évolution des autres CPUs. Le SuperSPARC était en avance sur son temps, ce quelques années, alors que l'HyperSPARC a un design de son temps.
Pour résumer, voici le tableau chronologique disant quel CPU utilisait quelle technique. Les CPU barrés sont des COU quadruple émission.
* Le partitionnement INT-FLOAT-MEM est en jaune.
* La double émission entière-flottante est en vert.
* La double émission entière est en rouge.
* Les processeurs sans couleur utilisaient soit de la triple émission, soit de la double émission un peu particulière, on reviendra dessus dans la suite.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| class="f_vert | RS/6000 || || || class="f_rouge | i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| class="f_vert | RSC || class="f_vert | PA 7100 || SuperSPARC || || class="f_jaune | 21064 ||
|-
! 1993
| class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || ||
|-
! 1994
| class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 ||
|-
! 1995
| 603/604 || <s>PA 8000</s> || <s>UltraSPARC 1</s> || Microarchitecture P6 (+ concurrence AMD) || || <s>MIPS R1000</s>
|}
C'est la même année , en 1994, que la quadruple émission est arrivée et que les CPU ont commencé à avoir de plus en plus d'unités de calcul. Même les designs restés en double émission avaient ajouté des unités de calcul. Il y avait alors trois stratégies pour la quadruple émission.
La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Les FPUs étaient soit dupliquées, soit scindées en deux en séparant l'additionneur du multiplieur flottant. Le POWER 2 dupliquait ses FPUs à la perfection, alors que l'21264 partitionnait la FPU. Les accès mémoire étaient traités comme des opérations entières, et le processeur pouvait : soit en faire un seul par cycle, soit en émettre deux, sous conditions (le cache devait être double port pour lire/écrire deux données à la fois, comme on le verra dans la suite du chapitre).
Les processeurs MIPS R8000 et R10000 allaient un peu plus loin, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités.
La seconde implémentation dupliquait l'ALU entière et combinait le tout avec un partitionnement INT-FLOAT-MEM. De quoi exécuter en même temps deux opérations entières, une opération flottante, et un accès mémoire. 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).
Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, avec au minimum deux ALU entières. Pour comprendre ce que je veux dire, prenons l'exemple de l'UltraSPARC. Il avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplier flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes. Le processeur pouvait émettre 4 instructions, tant qu'elles atterrissaient dans des unités différentes.
La même méthode peut être utilisée pour la double émission. Le Motorola 88110, sorti en 1991, était un CPU à double émission avec pas moins de 10 unités : deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise.
La morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes. Le tout est résumé dans le tableau ci-dessous, avec :
* en vert les CPU utilisant la première stratégie ;
* en rouge les CPU utilisant la seconde stratégie ;
* en jaune les CPU utilisant la troisième stratégie.
{|class="wikitable"
|+ Processeurs quadruple émission historiques
|-
! Année !! Processeur !! Unités
|-
! 1991
| class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques
|-
|-
! rowspan="2" | 1994
| class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'')
|-
| class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM
|-
! rowspan="3" | 1995
| class="f_jaune | PA 8000 || class="f_jaune | 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD
|-
| class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques
|-
| MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM
|-
! rowspan="2" | 1996
| class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM
|-
| class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL)
|}
Les CPU superscalaires modernes ont des design légèrement différents des CPU historiques. Ils dupliquent l'unité entière, mais leur partitionnement est différent. En général, les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Par exemple, un CPU à 6 voies pourra émettre/exécuter 6 opérations entières simultanément, grâce à 6 ALU entières. Mais il s'agit là d'un maximum et certaines peuvent être remplacées par une opération flottante ou mémoire, qui utilisent l'unité mémoire et/ou la FPU.
Un exemple assez parlant est les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon. C'était une microarchitecture à triple émission, avec trois ALU entières et trois unités mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements.
Il s'agit là d'une tendance, cependant. Les CPU superscalaires tendent à mettre le paquet niveau opérations arithmétiques, mais pas forcément au point de remplir la totalité de la largeur du processeur. La règle est que pour N voies, le processeur dispose d'au moins N/2 ALU entières. Attention, cela ne signifie pas que le processeur émet systématiquement N/2 opération entières. Il peut par exemple émettre une combinaison d'opérations flottantes et mémoire, avec des branchements. Et N/2 est une borne basse, la plupart des CPU x86 ont environ tournent plutôt autour des 2/3, voire des 3/4.
En général, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. La conséquence est que les unités mémoire sont dupliquées, en seconde place dans l'ordre d'importance. Les processeurs x86 ont commencé à dupliquer leurs unités mémoire dès qu'ils ont atteint la quadruple émission, pour les CPU grand public. La microarchitecture Core d'Intel utilisait la quadruple émission, avec deux unités mémoire et trois ALU entières. Les microarchitectures d'AMD K5 et K6 utilisaient aussi la quadruple émission et avaient deux ALUs pour deux unités mémoire.
Plus récemment, la microarchitecture ''Golden Cove'' dispose de cinq unités de calcul trois unités mémoire (deux spécialisées dans les écritures, trois dans les lectures). La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire.Il y a donc soit égalité, soit avantage pour les ALU entières.
Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair.
==L'unité d'émission d'un processeur superscalaire==
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Voyons pourquoi.
===L'excès de ports d'émission===
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ou 5 ?
Une solution est de réduire la largeur du processeur. Dans l'exemple précédent, il est possible de passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La solution précédente est certes très intéressante, mais elle a des défauts. Le principal est que les ports d'émission sont sous-utilisés. Et les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
===L'intérieur de l'unité d'émission===
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
==Le ''front-end'' des processeurs superscalaires==
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Le banc de registre d'un processeur superscalaire===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Une autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions auto-aligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions auto-aligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez longs, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
ivrq6u8pcqcsttvbhrk2lcwvpnv2no9
773125
773124
2026-09-25T15:38:06Z
Mewtow
31375
/* Le nombre de voies d'un processeur superscalaire */
773125
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===L'impact des contraintes d’appariement sur la performance===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour la suite du chapitre===
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==Les unités de calcul d'un processeur superscalaire==
Étudier les CPU superscalaires historique était une bonne introduction propédeutique, nous allons maintenant passer aux CPU superscalaires en général. Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Et cela nous permettra de généraliser certains concepts vus sur les CPU superscalaires historiques.
Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est possible d'exploiter plusieurs unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu.
Les tout premiers CPU superscalaires étaient nombreux à utiliser cette technique. Ils étaient des CPU double émission, avec une ALU entière, une unité mémoire et une FPU. Nous parlerons de '''partitionnement INT-FLOAT-MEM'''. Il s'agit de l'HyperSPARC, de l'alpha 21064 et du PowerPC 603. Les contrainte d’appariement variaient grandement suivant le processeur. Les plus permissifs autorisaient toutes les combinaisons possibles entre : une opération entière, une opération flottante, et une unité mémoire. Mais ils n'étaient pas les seuls.
Le PowerPC 603 implémentait ce genre de double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions).
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Le '''reconditionnement d'une unité fonctionnelle''' permet à celle-ci de faire des opérations qu'elle n'est pas prévue pour. Par exemple, il est possible d'utiliser l'unité mémoire comme seconde ALU entière, ou d'utiliser la FPU comme multiplieur. Ces deux exemples ne sont pas choisit au hasard, nous les étudierons dans ce qui suit. Le reconditionnement évite d'avoir à dupliquer une unité de calcul, tout en ajoutant une voie. Il permet d'améliorer l'efficacité du partitionnement, qu'il soit utilisé seul ou non.
Il est possible de reconditionner la FPU pour faire des multiplications entières. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits. Il est possible d'utiliser de multiplieur pour faire des multiplications entières 32 bits. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
Le reconditionnement de la FPU permet d'exécuter une multiplication entière en plus des autres opérations entières. Par exemple, prenons un CPU avec double émission entière-flottante, sans multiplieur entier séparé. Le reconditionnement de la FPU permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine.
Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants.
Passons maintenant à une seconde forme de reconditionnement. Reconditionner l'unité mémoire permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, des versions améliorées de l'Intel 960. L'Intel 960 originel n'était pas superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire au monde. Il intégrait une ALU entière, une unité mémoire et une unité de branchement. Grâce au partitionnement, il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, en remplacement d'une µop mémoire. Son pipeline permettait donc les combinaisons INT + BRANCH + (MEM / + INT*).
Le cas de l'i960 nous montre que le reconditionnement permet d'améliorer l'intérêt du partitionnement. Elle permet de ne pas dupliquer d'unité de calcul, tout en permettant quand même d'émettre une seconde opération entière.
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''.
Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières ! Il doit y avoir deux ALU entières pour de la double émission entière, trois pour de triple émission éntière, 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]]
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications.
Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que dupliquer l'ALU entière peut se marier parfaitement avec la présence d'une FPU. Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU reconditionne sa FPU ou pas. Mais il est très rare qu'un CPU ait à la fois un multiplieur entier et une FPU reconditionnée.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
Le processeur SuperSPARC avait deux ALU entière et une FPU. La FPU était reconditionnée, ce qui fait que la multiplication était faite dans la FPU, idem pour la division. Il lui est possible d'émettre deux opérations entières simples, en plus d'une multiplication. Par contre, on perd la possibilité d'émettre en même temps une multiplication et une opération flottante. Les combinaisons INT + INT + (MUL/FLOAT) sont possibles pas les combinaisons INT + MUL + FPU.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires modernes utilisent toutes ces stratégies===
Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, duplication de l'ALU entière, duplication des autre unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents.
* Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU.
* Le '''reconditionnement des unités''' a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU.
* La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants.
* La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables.
De nos jours, le partitionnement est utilisé en complément de la duplication des ALU entières, et de la duplication des autres unités. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993.
Le meilleur moyen de voir comment sont combinés ces quatre technique est de regarder ce qu'il s'est passé historiquement.
==L'évolution historique de la superscalarité==
Pour commencer, étudions le cas des premiers CPU superscalaires à double émission. Ils sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Notamment, la duplication des autres unités ne sert pour ainsi dire à rien, ce qui limite les possibilités, mais rendra les explications plus simples.
Ces modèles utilisaient presque tous la double émission avec trois unités : une ALU, une FPU et une unité mémoire. Si la moitié des processeurs faisait de la double émission "complète", l'autre moitié utilisait une forme encore plus simple de partitionnement.
La '''double émission entière-flottante''' consiste à émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le processeur a donc deux pipelines : un pipeline entier, et un pipeline flottant. Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire, vu que les µops mémoire passent par l'ALU entière avant d’atterrir dans l'unité mémoire. Le reconditionnement de la FPU était utilisé sur les processeurs à double émission entière-flottante. Il entrainait un léger en performance, pour un cout en circuits presque nul.
{|
|[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU.]]
|[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
|}
D'autres processeurs utilisaient uniquement le reconditionnement, mais séparaient l'unité mémoire de l'ALU entière. Ils faisaient donc du '''partitionnement INT-FLOAT-MEM''', avec des contraintes d'appariement relâchées comparé à l'émission entière-flottante. Sur les deux types précédents de CPU, le reconditionnement des unités était souvent utilisé, mais pas systématique. La moitié des processeurs de l'époque l'utilisaient, pas l'autre. Le reconditionnement de l'AGU aurait pu être utilisé, mais ne l'a été que sur l'Intel i960. Ce qui nous amène à parler des autres CPUs.
D'autres CPU superscalaires de l'époque utilisaient la '''double émission entière''', à savoir qu'ils peuvent émettre une opération en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Pour cela, ils peuvent dupliquer une ALU entière, ou reconditionner l'unité mémoire. Les exemples classiques sont les processeurs Intel : i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 reconditionnait l'unité mémoire en ALU entière.
L'Intel i960 utilisait le reconditionnement de l'AGU en plus du partitionnement, ce qui lui permettait d'exécuter une seconde instruction entière, en plus d'une autre instruction. Les instructions en question étaient simples : additions, quelques décalages. De nos jours, ce combo partitionnement et reconditionnement de l'AGU n'est plus utilisé. Il n'a pas beaucoup de sens quand on peut dupliquer des ALU entières, et les processeurs récents ont un budget en transistor suffisant pour.
Le Pentium dupliquait l'ALU entière, mais n'utilisaient aucune forme de partitionnement. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Contrairement au i960, le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
De rares CPUs ont tenté d'utiliser à la fois le partitionnement et la double émission entière. Le résultat utilisait soit la double, soit la triple émission. Les deux CPUs en question sont le PA-RISC PA 7200 d'HP, et le SuperSPARC.
Le PA 7200, sorti en 1994, est un PA-7100 auquel on aurait rajouté une ALU entière. Précisons que ce processeur utilisait aussi le reconditionnement de la FPU, comme son prédécesseur. Les combinaisons autorisées étaient donc INT + INT, INT + MEM, INT + FLOAT/MUL, MEM + FLOAT/MUL. Les combinaisons autorisées par le partitionnement INT-FLOAT-MEM étaient donc possibles, en plus des combinaisons INT + INT, voire INT + INT + MUL (vu que la FPU est reconditionnée). Autrement dit, il pouvait faire toute ce que permettait un CPU à double émission entière, plus les combinaisons FLOAT/MUL + MEM.
Le SuperSPARC était un CPU triple émission, avec deux ALU entières. Il pouvait émettre, à chaque cycle, trois µops en piochant dans deux opérations entières, un accès mémoire, une opération flottante. Le SuperSPARC avait de nombreuses contraintes d’appariement. Par exemple, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Une bonne partie de ces contraintes était lié au banc de registre, qui n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions.
: La seconde ALU a été abandonnée dans l'HyperSPARC, qui est de plus repassé en double émission, ce qui est une évolution à rebours de l'évolution des autres CPUs. Le SuperSPARC était en avance sur son temps, ce quelques années, alors que l'HyperSPARC a un design de son temps.
Pour résumer, voici le tableau chronologique disant quel CPU utilisait quelle technique. Les CPU barrés sont des COU quadruple émission.
* Le partitionnement INT-FLOAT-MEM est en jaune.
* La double émission entière-flottante est en vert.
* La double émission entière est en rouge.
* Les processeurs sans couleur utilisaient soit de la triple émission, soit de la double émission un peu particulière, on reviendra dessus dans la suite.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| class="f_vert | RS/6000 || || || class="f_rouge | i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| class="f_vert | RSC || class="f_vert | PA 7100 || SuperSPARC || || class="f_jaune | 21064 ||
|-
! 1993
| class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || ||
|-
! 1994
| class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 ||
|-
! 1995
| 603/604 || <s>PA 8000</s> || <s>UltraSPARC 1</s> || Microarchitecture P6 (+ concurrence AMD) || || <s>MIPS R1000</s>
|}
C'est la même année , en 1994, que la quadruple émission est arrivée et que les CPU ont commencé à avoir de plus en plus d'unités de calcul. Même les designs restés en double émission avaient ajouté des unités de calcul. Il y avait alors trois stratégies pour la quadruple émission.
La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Les FPUs étaient soit dupliquées, soit scindées en deux en séparant l'additionneur du multiplieur flottant. Le POWER 2 dupliquait ses FPUs à la perfection, alors que l'21264 partitionnait la FPU. Les accès mémoire étaient traités comme des opérations entières, et le processeur pouvait : soit en faire un seul par cycle, soit en émettre deux, sous conditions (le cache devait être double port pour lire/écrire deux données à la fois, comme on le verra dans la suite du chapitre).
Les processeurs MIPS R8000 et R10000 allaient un peu plus loin, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités.
La seconde implémentation dupliquait l'ALU entière et combinait le tout avec un partitionnement INT-FLOAT-MEM. De quoi exécuter en même temps deux opérations entières, une opération flottante, et un accès mémoire. 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).
Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, avec au minimum deux ALU entières. Pour comprendre ce que je veux dire, prenons l'exemple de l'UltraSPARC. Il avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplier flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes. Le processeur pouvait émettre 4 instructions, tant qu'elles atterrissaient dans des unités différentes.
La même méthode peut être utilisée pour la double émission. Le Motorola 88110, sorti en 1991, était un CPU à double émission avec pas moins de 10 unités : deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise.
La morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes. Le tout est résumé dans le tableau ci-dessous, avec :
* en vert les CPU utilisant la première stratégie ;
* en rouge les CPU utilisant la seconde stratégie ;
* en jaune les CPU utilisant la troisième stratégie.
{|class="wikitable"
|+ Processeurs quadruple émission historiques
|-
! Année !! Processeur !! Unités
|-
! 1991
| class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques
|-
|-
! rowspan="2" | 1994
| class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'')
|-
| class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM
|-
! rowspan="3" | 1995
| class="f_jaune | PA 8000 || class="f_jaune | 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD
|-
| class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques
|-
| MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM
|-
! rowspan="2" | 1996
| class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM
|-
| class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL)
|}
Les CPU superscalaires modernes ont des design légèrement différents des CPU historiques. Ils dupliquent l'unité entière, mais leur partitionnement est différent. En général, les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Par exemple, un CPU à 6 voies pourra émettre/exécuter 6 opérations entières simultanément, grâce à 6 ALU entières. Mais il s'agit là d'un maximum et certaines peuvent être remplacées par une opération flottante ou mémoire, qui utilisent l'unité mémoire et/ou la FPU.
Un exemple assez parlant est les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon. C'était une microarchitecture à triple émission, avec trois ALU entières et trois unités mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements.
Il s'agit là d'une tendance, cependant. Les CPU superscalaires tendent à mettre le paquet niveau opérations arithmétiques, mais pas forcément au point de remplir la totalité de la largeur du processeur. La règle est que pour N voies, le processeur dispose d'au moins N/2 ALU entières. Attention, cela ne signifie pas que le processeur émet systématiquement N/2 opération entières. Il peut par exemple émettre une combinaison d'opérations flottantes et mémoire, avec des branchements. Et N/2 est une borne basse, la plupart des CPU x86 ont environ tournent plutôt autour des 2/3, voire des 3/4.
En général, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. La conséquence est que les unités mémoire sont dupliquées, en seconde place dans l'ordre d'importance. Les processeurs x86 ont commencé à dupliquer leurs unités mémoire dès qu'ils ont atteint la quadruple émission, pour les CPU grand public. La microarchitecture Core d'Intel utilisait la quadruple émission, avec deux unités mémoire et trois ALU entières. Les microarchitectures d'AMD K5 et K6 utilisaient aussi la quadruple émission et avaient deux ALUs pour deux unités mémoire.
Plus récemment, la microarchitecture ''Golden Cove'' dispose de cinq unités de calcul trois unités mémoire (deux spécialisées dans les écritures, trois dans les lectures). La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire.Il y a donc soit égalité, soit avantage pour les ALU entières.
Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair.
==L'unité d'émission d'un processeur superscalaire==
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Voyons pourquoi.
===L'excès de ports d'émission===
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ou 5 ?
Une solution est de réduire la largeur du processeur. Dans l'exemple précédent, il est possible de passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La solution précédente est certes très intéressante, mais elle a des défauts. Le principal est que les ports d'émission sont sous-utilisés. Et les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
===L'intérieur de l'unité d'émission===
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
==Le ''front-end'' des processeurs superscalaires==
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Le banc de registre d'un processeur superscalaire===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Une autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions auto-aligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions auto-aligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez longs, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
195aas0wpjhyputofi6zovwrz9vqihg
773126
773125
2026-09-25T15:42:12Z
Mewtow
31375
/* L'évolution historique de la superscalarité */
773126
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===L'impact des contraintes d’appariement sur la performance===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour la suite du chapitre===
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==Les unités de calcul d'un processeur superscalaire==
Étudier les CPU superscalaires historique était une bonne introduction propédeutique, nous allons maintenant passer aux CPU superscalaires en général. Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Et cela nous permettra de généraliser certains concepts vus sur les CPU superscalaires historiques.
Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est possible d'exploiter plusieurs unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu.
Les tout premiers CPU superscalaires étaient nombreux à utiliser cette technique. Ils étaient des CPU double émission, avec une ALU entière, une unité mémoire et une FPU. Nous parlerons de '''partitionnement INT-FLOAT-MEM'''. Il s'agit de l'HyperSPARC, de l'alpha 21064 et du PowerPC 603. Les contrainte d’appariement variaient grandement suivant le processeur. Les plus permissifs autorisaient toutes les combinaisons possibles entre : une opération entière, une opération flottante, et une unité mémoire. Mais ils n'étaient pas les seuls.
Le PowerPC 603 implémentait ce genre de double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions).
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Le '''reconditionnement d'une unité fonctionnelle''' permet à celle-ci de faire des opérations qu'elle n'est pas prévue pour. Par exemple, il est possible d'utiliser l'unité mémoire comme seconde ALU entière, ou d'utiliser la FPU comme multiplieur. Ces deux exemples ne sont pas choisit au hasard, nous les étudierons dans ce qui suit. Le reconditionnement évite d'avoir à dupliquer une unité de calcul, tout en ajoutant une voie. Il permet d'améliorer l'efficacité du partitionnement, qu'il soit utilisé seul ou non.
Il est possible de reconditionner la FPU pour faire des multiplications entières. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits. Il est possible d'utiliser de multiplieur pour faire des multiplications entières 32 bits. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
Le reconditionnement de la FPU permet d'exécuter une multiplication entière en plus des autres opérations entières. Par exemple, prenons un CPU avec double émission entière-flottante, sans multiplieur entier séparé. Le reconditionnement de la FPU permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine.
Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants.
Passons maintenant à une seconde forme de reconditionnement. Reconditionner l'unité mémoire permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, des versions améliorées de l'Intel 960. L'Intel 960 originel n'était pas superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire au monde. Il intégrait une ALU entière, une unité mémoire et une unité de branchement. Grâce au partitionnement, il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, en remplacement d'une µop mémoire. Son pipeline permettait donc les combinaisons INT + BRANCH + (MEM / + INT*).
Le cas de l'i960 nous montre que le reconditionnement permet d'améliorer l'intérêt du partitionnement. Elle permet de ne pas dupliquer d'unité de calcul, tout en permettant quand même d'émettre une seconde opération entière.
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''.
Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières ! Il doit y avoir deux ALU entières pour de la double émission entière, trois pour de triple émission éntière, 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]]
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications.
Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que dupliquer l'ALU entière peut se marier parfaitement avec la présence d'une FPU. Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU reconditionne sa FPU ou pas. Mais il est très rare qu'un CPU ait à la fois un multiplieur entier et une FPU reconditionnée.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
Le processeur SuperSPARC avait deux ALU entière et une FPU. La FPU était reconditionnée, ce qui fait que la multiplication était faite dans la FPU, idem pour la division. Il lui est possible d'émettre deux opérations entières simples, en plus d'une multiplication. Par contre, on perd la possibilité d'émettre en même temps une multiplication et une opération flottante. Les combinaisons INT + INT + (MUL/FLOAT) sont possibles pas les combinaisons INT + MUL + FPU.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires modernes utilisent toutes ces stratégies===
Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, duplication de l'ALU entière, duplication des autre unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents.
* Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU.
* Le '''reconditionnement des unités''' a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU.
* La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants.
* La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables.
De nos jours, le partitionnement est utilisé en complément de la duplication des ALU entières, et de la duplication des autres unités. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993.
Le meilleur moyen de voir comment sont combinés ces quatre technique est de regarder ce qu'il s'est passé historiquement.
==L'évolution historique de la superscalarité==
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. La plupart des CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 sont rapidement montés à de la quadruple émission, avec quelques exceptions. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 ||
|-
! 1995
| 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000
|-
! 1996
| class="f_bleu | 620 || || || || class="f_bleu | 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
===Les processeurs historiques à double émission===
Pour commencer, étudions le cas des premiers CPU superscalaires à double émission. Ils sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Notamment, la duplication des autres unités ne sert pour ainsi dire à rien, ce qui limite les possibilités, mais rendra les explications plus simples.
Il pouvaient se classer en trois types, assez stricts. Le premier utilise le partitionnement seul, le second dupliquer les ALU entières sans partitionnement, et le troisième type est volontairement passé sous silence pour le moment (nous aborderons le troisième type dans la partie sur des design à quadruple émission).
Les CPU n'utilisant que le partitionnement faisaient la double émission avec trois unités : une ALU, une FPU et une unité mémoire. Le reconditionnement de la FPU était parfois utilisé, mais l'AGU n'était jamais reconditionnée. Si la moitié des processeurs faisait de la double émission "complète", l'autre moitié utilisait une forme encore plus simple de partitionnement.
La '''double émission entière-flottante''' consiste à émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le processeur a donc deux pipelines : un pipeline entier, et un pipeline flottant. Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire, vu que les µops mémoire passent par l'ALU entière avant d’atterrir dans l'unité mémoire. Le reconditionnement de la FPU était utilisé sur les processeurs à double émission entière-flottante. Il entrainait un léger en performance, pour un cout en circuits presque nul.
{|
|[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU.]]
|[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
|}
La seconde classe de CPU superscalaires de l'époque utilisait la '''double émission entière''', à savoir qu'ils peuvent émettre une opération en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Pour cela, ils peuvent dupliquer une ALU entière, ou reconditionner l'unité mémoire. Les exemples classiques sont les processeurs Intel : i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 reconditionnait l'unité mémoire en ALU entière.
L'Intel i960 utilisait le reconditionnement de l'AGU en plus du partitionnement, ce qui lui permettait d'exécuter une seconde instruction entière, en plus d'une autre instruction. Les instructions en question étaient simples : additions, quelques décalages. De nos jours, ce combo partitionnement et reconditionnement de l'AGU n'est plus utilisé. Il n'a pas beaucoup de sens quand on peut dupliquer des ALU entières, et les processeurs récents ont un budget en transistor suffisant pour.
Le Pentium dupliquait l'ALU entière, mais n'utilisaient aucune forme de partitionnement. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Contrairement au i960, le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
De rares CPUs ont tenté d'utiliser à la fois le partitionnement et la double émission entière. Le résultat utilisait soit la double, soit la triple émission. Les deux CPUs en question sont le PA-RISC PA 7200 d'HP, et le SuperSPARC.
Le PA 7200, sorti en 1994, est un PA-7100 auquel on aurait rajouté une ALU entière. Précisons que ce processeur utilisait aussi le reconditionnement de la FPU, comme son prédécesseur. Les combinaisons autorisées étaient donc INT + INT, INT + MEM, INT + FLOAT/MUL, MEM + FLOAT/MUL. Les combinaisons autorisées par le partitionnement INT-FLOAT-MEM étaient donc possibles, en plus des combinaisons INT + INT, voire INT + INT + MUL (vu que la FPU est reconditionnée). Autrement dit, il pouvait faire toute ce que permettait un CPU à double émission entière, plus les combinaisons FLOAT/MUL + MEM.
Le SuperSPARC était un CPU triple émission, avec deux ALU entières. Il pouvait émettre, à chaque cycle, trois µops en piochant dans deux opérations entières, un accès mémoire, une opération flottante. Le SuperSPARC avait de nombreuses contraintes d’appariement. Par exemple, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Une bonne partie de ces contraintes était lié au banc de registre, qui n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions.
: La seconde ALU a été abandonnée dans l'HyperSPARC, qui est de plus repassé en double émission, ce qui est une évolution à rebours de l'évolution des autres CPUs. Le SuperSPARC était en avance sur son temps, ce quelques années, alors que l'HyperSPARC a un design de son temps.
Pour résumer, voici le tableau chronologique disant quel CPU utilisait quelle technique. Les CPU barrés sont des COU quadruple émission.
* Le partitionnement INT-FLOAT-MEM est en jaune.
* La double émission entière-flottante est en vert.
* La double émission entière est en rouge.
* Les processeurs sans couleur utilisaient soit de la triple émission, soit de la double émission un peu particulière, on reviendra dessus dans la suite.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| class="f_vert | RS/6000 || || || class="f_rouge | i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| class="f_vert | RSC || class="f_vert | PA 7100 || SuperSPARC || || class="f_jaune | 21064 ||
|-
! 1993
| class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || ||
|-
! 1994
| class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 ||
|-
! 1995
| 603/604 || <s>PA 8000</s> || <s>UltraSPARC 1</s> || Microarchitecture P6 (+ concurrence AMD) || || <s>MIPS R1000</s>
|}
===Les processeurs historiques à quadruple émission===
C'est la même année , en 1994, que la quadruple émission est arrivée et que les CPU ont commencé à avoir de plus en plus d'unités de calcul. Même les designs restés en double émission avaient ajouté des unités de calcul. Il y avait alors trois stratégies pour la quadruple émission.
La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Les FPUs étaient soit dupliquées, soit scindées en deux en séparant l'additionneur du multiplieur flottant. Le POWER 2 dupliquait ses FPUs à la perfection, alors que l'21264 partitionnait la FPU. Les accès mémoire étaient traités comme des opérations entières, et le processeur pouvait : soit en faire un seul par cycle, soit en émettre deux, sous conditions (le cache devait être double port pour lire/écrire deux données à la fois, comme on le verra dans la suite du chapitre).
Les processeurs MIPS R8000 et R10000 allaient un peu plus loin, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités.
La seconde implémentation dupliquait l'ALU entière et combinait le tout avec un partitionnement INT-FLOAT-MEM. De quoi exécuter en même temps deux opérations entières, une opération flottante, et un accès mémoire. 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).
Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, avec au minimum deux ALU entières. Pour comprendre ce que je veux dire, prenons l'exemple de l'UltraSPARC. Il avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplier flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes. Le processeur pouvait émettre 4 instructions, tant qu'elles atterrissaient dans des unités différentes.
La même méthode peut être utilisée pour la double émission. Le Motorola 88110, sorti en 1991, était un CPU à double émission avec pas moins de 10 unités : deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise.
La morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes. Le tout est résumé dans le tableau ci-dessous, avec :
* en vert les CPU utilisant la première stratégie ;
* en rouge les CPU utilisant la seconde stratégie ;
* en jaune les CPU utilisant la troisième stratégie.
{|class="wikitable"
|+ Processeurs quadruple émission historiques
|-
! Année !! Processeur !! Unités
|-
! 1991
| class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques
|-
|-
! rowspan="2" | 1994
| class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'')
|-
| class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM
|-
! rowspan="3" | 1995
| class="f_jaune | PA 8000 || class="f_jaune | 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD
|-
| class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques
|-
| MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM
|-
! rowspan="2" | 1996
| class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM
|-
| class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL)
|}
===Les processeurs superscalaires modernes===
Les CPU superscalaires modernes ont des design légèrement différents des CPU historiques. Ils dupliquent l'unité entière, mais leur partitionnement est différent. En général, les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Par exemple, un CPU à 6 voies pourra émettre/exécuter 6 opérations entières simultanément, grâce à 6 ALU entières. Mais il s'agit là d'un maximum et certaines peuvent être remplacées par une opération flottante ou mémoire, qui utilisent l'unité mémoire et/ou la FPU.
Un exemple assez parlant est les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon. C'était une microarchitecture à triple émission, avec trois ALU entières et trois unités mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements.
Il s'agit là d'une tendance, cependant. Les CPU superscalaires tendent à mettre le paquet niveau opérations arithmétiques, mais pas forcément au point de remplir la totalité de la largeur du processeur. La règle est que pour N voies, le processeur dispose d'au moins N/2 ALU entières. Attention, cela ne signifie pas que le processeur émet systématiquement N/2 opération entières. Il peut par exemple émettre une combinaison d'opérations flottantes et mémoire, avec des branchements. Et N/2 est une borne basse, la plupart des CPU x86 ont environ tournent plutôt autour des 2/3, voire des 3/4.
En général, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. La conséquence est que les unités mémoire sont dupliquées, en seconde place dans l'ordre d'importance. Les processeurs x86 ont commencé à dupliquer leurs unités mémoire dès qu'ils ont atteint la quadruple émission, pour les CPU grand public. La microarchitecture Core d'Intel utilisait la quadruple émission, avec deux unités mémoire et trois ALU entières. Les microarchitectures d'AMD K5 et K6 utilisaient aussi la quadruple émission et avaient deux ALUs pour deux unités mémoire.
Plus récemment, la microarchitecture ''Golden Cove'' dispose de cinq unités de calcul trois unités mémoire (deux spécialisées dans les écritures, trois dans les lectures). La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire.Il y a donc soit égalité, soit avantage pour les ALU entières.
Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair.
==L'unité d'émission d'un processeur superscalaire==
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Voyons pourquoi.
===L'excès de ports d'émission===
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ou 5 ?
Une solution est de réduire la largeur du processeur. Dans l'exemple précédent, il est possible de passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La solution précédente est certes très intéressante, mais elle a des défauts. Le principal est que les ports d'émission sont sous-utilisés. Et les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
===L'intérieur de l'unité d'émission===
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
==Le ''front-end'' des processeurs superscalaires==
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Le banc de registre d'un processeur superscalaire===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Une autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions auto-aligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions auto-aligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez longs, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
50z6i1gdg2id12xohl7i4jzwej279hj
773127
773126
2026-09-25T15:45:21Z
Mewtow
31375
/* Les processeurs historiques à double émission */
773127
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===L'impact des contraintes d’appariement sur la performance===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour la suite du chapitre===
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==Les unités de calcul d'un processeur superscalaire==
Étudier les CPU superscalaires historique était une bonne introduction propédeutique, nous allons maintenant passer aux CPU superscalaires en général. Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Et cela nous permettra de généraliser certains concepts vus sur les CPU superscalaires historiques.
Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est possible d'exploiter plusieurs unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu.
Les tout premiers CPU superscalaires étaient nombreux à utiliser cette technique. Ils étaient des CPU double émission, avec une ALU entière, une unité mémoire et une FPU. Nous parlerons de '''partitionnement INT-FLOAT-MEM'''. Il s'agit de l'HyperSPARC, de l'alpha 21064 et du PowerPC 603. Les contrainte d’appariement variaient grandement suivant le processeur. Les plus permissifs autorisaient toutes les combinaisons possibles entre : une opération entière, une opération flottante, et une unité mémoire. Mais ils n'étaient pas les seuls.
Le PowerPC 603 implémentait ce genre de double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions).
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Le '''reconditionnement d'une unité fonctionnelle''' permet à celle-ci de faire des opérations qu'elle n'est pas prévue pour. Par exemple, il est possible d'utiliser l'unité mémoire comme seconde ALU entière, ou d'utiliser la FPU comme multiplieur. Ces deux exemples ne sont pas choisit au hasard, nous les étudierons dans ce qui suit. Le reconditionnement évite d'avoir à dupliquer une unité de calcul, tout en ajoutant une voie. Il permet d'améliorer l'efficacité du partitionnement, qu'il soit utilisé seul ou non.
Il est possible de reconditionner la FPU pour faire des multiplications entières. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits. Il est possible d'utiliser de multiplieur pour faire des multiplications entières 32 bits. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
Le reconditionnement de la FPU permet d'exécuter une multiplication entière en plus des autres opérations entières. Par exemple, prenons un CPU avec double émission entière-flottante, sans multiplieur entier séparé. Le reconditionnement de la FPU permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine.
Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants.
Passons maintenant à une seconde forme de reconditionnement. Reconditionner l'unité mémoire permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, des versions améliorées de l'Intel 960. L'Intel 960 originel n'était pas superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire au monde. Il intégrait une ALU entière, une unité mémoire et une unité de branchement. Grâce au partitionnement, il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, en remplacement d'une µop mémoire. Son pipeline permettait donc les combinaisons INT + BRANCH + (MEM / + INT*).
Le cas de l'i960 nous montre que le reconditionnement permet d'améliorer l'intérêt du partitionnement. Elle permet de ne pas dupliquer d'unité de calcul, tout en permettant quand même d'émettre une seconde opération entière.
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''.
Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières ! Il doit y avoir deux ALU entières pour de la double émission entière, trois pour de triple émission éntière, 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]]
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications.
Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que dupliquer l'ALU entière peut se marier parfaitement avec la présence d'une FPU. Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU reconditionne sa FPU ou pas. Mais il est très rare qu'un CPU ait à la fois un multiplieur entier et une FPU reconditionnée.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
Le processeur SuperSPARC avait deux ALU entière et une FPU. La FPU était reconditionnée, ce qui fait que la multiplication était faite dans la FPU, idem pour la division. Il lui est possible d'émettre deux opérations entières simples, en plus d'une multiplication. Par contre, on perd la possibilité d'émettre en même temps une multiplication et une opération flottante. Les combinaisons INT + INT + (MUL/FLOAT) sont possibles pas les combinaisons INT + MUL + FPU.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires modernes utilisent toutes ces stratégies===
Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, duplication de l'ALU entière, duplication des autre unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents.
* Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU.
* Le '''reconditionnement des unités''' a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU.
* La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants.
* La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables.
De nos jours, le partitionnement est utilisé en complément de la duplication des ALU entières, et de la duplication des autres unités. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993.
Le meilleur moyen de voir comment sont combinés ces quatre technique est de regarder ce qu'il s'est passé historiquement.
==L'évolution historique de la superscalarité==
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. La plupart des CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 sont rapidement montés à de la quadruple émission, avec quelques exceptions. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 ||
|-
! 1995
| 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000
|-
! 1996
| class="f_bleu | 620 || || || || class="f_bleu | 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
===Les processeurs historiques à double émission===
Pour commencer, étudions le cas des premiers CPU superscalaires à double émission. Ils sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Notamment, la duplication des autres unités ne sert pour ainsi dire à rien, ce qui limite les possibilités, mais rendra les explications plus simples.
Il pouvaient se classer en deux/trois types principaux, assez stricts, avec quelques processeurs qui ne rentraient pas dans ces cases. Le premier utilise le partitionnement seul, le second dupliquer les ALU entières sans partitionnement, et le troisième type est volontairement passé sous silence pour le moment (nous aborderons le troisième type dans la partie sur des design à quadruple émission).
Les CPU n'utilisant que le partitionnement faisaient la double émission avec trois unités : une ALU, une FPU et une unité mémoire. Le reconditionnement de la FPU était parfois utilisé, mais l'AGU n'était jamais reconditionnée. Si la moitié des processeurs faisait du ''partitionnement complet'' entre ALU, FPU et unité mémoire, l'autre moitié utilisait une forme encore plus simple de partitionnement.
La '''double émission entière-flottante''' consiste à émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le processeur a donc deux pipelines : un pipeline entier, et un pipeline flottant. Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire, vu que les µops mémoire passent par l'ALU entière avant d’atterrir dans l'unité mémoire. Le reconditionnement de la FPU était utilisé sur les processeurs à double émission entière-flottante. Il entrainait un léger en performance, pour un cout en circuits presque nul.
{|
|[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU.]]
|[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
|}
La seconde classe de CPU superscalaires de l'époque utilisait la '''double émission entière''', à savoir qu'ils peuvent émettre une opération en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Pour cela, ils peuvent dupliquer une ALU entière, ou reconditionner l'unité mémoire. Les exemples classiques sont les processeurs Intel : i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 reconditionnait l'unité mémoire en ALU entière.
L'Intel i960 utilisait le reconditionnement de l'AGU en plus du partitionnement, ce qui lui permettait d'exécuter une seconde instruction entière, en plus d'une autre instruction. Les instructions en question étaient simples : additions, quelques décalages. De nos jours, ce combo partitionnement et reconditionnement de l'AGU n'est plus utilisé. Il n'a pas beaucoup de sens quand on peut dupliquer des ALU entières, et les processeurs récents ont un budget en transistor suffisant pour.
Le Pentium dupliquait l'ALU entière, mais n'utilisaient aucune forme de partitionnement. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Contrairement au i960, le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
Pour résumer, voici le tableau chronologique disant quel CPU utilisait quelle technique. Les CPU barrés sont des COU quadruple émission.
* Le partitionnement INT-FLOAT-MEM est en jaune.
* La double émission entière-flottante est en vert.
* La double émission entière est en rouge.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| class="f_vert | RS/6000 || || || class="f_rouge | i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| class="f_vert | RSC || class="f_vert | PA 7100 || SuperSPARC || || class="f_jaune | 21064 ||
|-
! 1993
| class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || ||
|-
! 1994
| class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 ||
|-
! 1995
| 603/604 || <s>PA 8000</s> || <s>UltraSPARC 1</s> || Microarchitecture P6 (+ concurrence AMD) || || <s>MIPS R1000</s>
|}
Vous voyez que certains processeur sont laissés de côté dans le tableau précédent. C'est car ils ne rentrent pas dans les catégories précédentes et faisaient autrement. Si on omet le Motorola 88110, qui utilisait une forme de double émission assez avancée, les deux CPUs qui vont nous intéresser sont le PA-RISC PA 7200 d'HP, et le SuperSPARC.
Le PA 7200, sorti en 1994, est un PA-7100 auquel on aurait rajouté une ALU entière. Précisons que ce processeur utilisait aussi le reconditionnement de la FPU, comme son prédécesseur. Les combinaisons autorisées étaient donc INT + INT, INT + MEM, INT + FLOAT/MUL, MEM + FLOAT/MUL. Les combinaisons autorisées par le partitionnement INT-FLOAT-MEM étaient donc possibles, en plus des combinaisons INT + INT, voire INT + INT + MUL (vu que la FPU est reconditionnée). Autrement dit, il pouvait faire toute ce que permettait un CPU à double émission entière, plus les combinaisons FLOAT/MUL + MEM.
Le SuperSPARC était un CPU triple émission, avec deux ALU entières. Il pouvait émettre, à chaque cycle, trois µops en piochant dans deux opérations entières, un accès mémoire, une opération flottante. Le SuperSPARC avait de nombreuses contraintes d’appariement. Par exemple, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Une bonne partie de ces contraintes était lié au banc de registre, qui n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions.
: La seconde ALU a été abandonnée dans l'HyperSPARC, qui est de plus repassé en double émission, ce qui est une évolution à rebours de l'évolution des autres CPUs. Le SuperSPARC était en avance sur son temps, ce quelques années, alors que l'HyperSPARC a un design de son temps.
===Les processeurs historiques à quadruple émission===
C'est la même année , en 1994, que la quadruple émission est arrivée et que les CPU ont commencé à avoir de plus en plus d'unités de calcul. Même les designs restés en double émission avaient ajouté des unités de calcul. Il y avait alors trois stratégies pour la quadruple émission.
La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Les FPUs étaient soit dupliquées, soit scindées en deux en séparant l'additionneur du multiplieur flottant. Le POWER 2 dupliquait ses FPUs à la perfection, alors que l'21264 partitionnait la FPU. Les accès mémoire étaient traités comme des opérations entières, et le processeur pouvait : soit en faire un seul par cycle, soit en émettre deux, sous conditions (le cache devait être double port pour lire/écrire deux données à la fois, comme on le verra dans la suite du chapitre).
Les processeurs MIPS R8000 et R10000 allaient un peu plus loin, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités.
La seconde implémentation dupliquait l'ALU entière et combinait le tout avec un partitionnement INT-FLOAT-MEM. De quoi exécuter en même temps deux opérations entières, une opération flottante, et un accès mémoire. 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).
Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, avec au minimum deux ALU entières. Pour comprendre ce que je veux dire, prenons l'exemple de l'UltraSPARC. Il avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplier flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes. Le processeur pouvait émettre 4 instructions, tant qu'elles atterrissaient dans des unités différentes.
La même méthode peut être utilisée pour la double émission. Le Motorola 88110, sorti en 1991, était un CPU à double émission avec pas moins de 10 unités : deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise.
La morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes. Le tout est résumé dans le tableau ci-dessous, avec :
* en vert les CPU utilisant la première stratégie ;
* en rouge les CPU utilisant la seconde stratégie ;
* en jaune les CPU utilisant la troisième stratégie.
{|class="wikitable"
|+ Processeurs quadruple émission historiques
|-
! Année !! Processeur !! Unités
|-
! 1991
| class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques
|-
|-
! rowspan="2" | 1994
| class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'')
|-
| class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM
|-
! rowspan="3" | 1995
| class="f_jaune | PA 8000 || class="f_jaune | 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD
|-
| class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques
|-
| MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM
|-
! rowspan="2" | 1996
| class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM
|-
| class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL)
|}
===Les processeurs superscalaires modernes===
Les CPU superscalaires modernes ont des design légèrement différents des CPU historiques. Ils dupliquent l'unité entière, mais leur partitionnement est différent. En général, les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Par exemple, un CPU à 6 voies pourra émettre/exécuter 6 opérations entières simultanément, grâce à 6 ALU entières. Mais il s'agit là d'un maximum et certaines peuvent être remplacées par une opération flottante ou mémoire, qui utilisent l'unité mémoire et/ou la FPU.
Un exemple assez parlant est les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon. C'était une microarchitecture à triple émission, avec trois ALU entières et trois unités mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements.
Il s'agit là d'une tendance, cependant. Les CPU superscalaires tendent à mettre le paquet niveau opérations arithmétiques, mais pas forcément au point de remplir la totalité de la largeur du processeur. La règle est que pour N voies, le processeur dispose d'au moins N/2 ALU entières. Attention, cela ne signifie pas que le processeur émet systématiquement N/2 opération entières. Il peut par exemple émettre une combinaison d'opérations flottantes et mémoire, avec des branchements. Et N/2 est une borne basse, la plupart des CPU x86 ont environ tournent plutôt autour des 2/3, voire des 3/4.
En général, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. La conséquence est que les unités mémoire sont dupliquées, en seconde place dans l'ordre d'importance. Les processeurs x86 ont commencé à dupliquer leurs unités mémoire dès qu'ils ont atteint la quadruple émission, pour les CPU grand public. La microarchitecture Core d'Intel utilisait la quadruple émission, avec deux unités mémoire et trois ALU entières. Les microarchitectures d'AMD K5 et K6 utilisaient aussi la quadruple émission et avaient deux ALUs pour deux unités mémoire.
Plus récemment, la microarchitecture ''Golden Cove'' dispose de cinq unités de calcul trois unités mémoire (deux spécialisées dans les écritures, trois dans les lectures). La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire.Il y a donc soit égalité, soit avantage pour les ALU entières.
Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair.
==L'unité d'émission d'un processeur superscalaire==
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Voyons pourquoi.
===L'excès de ports d'émission===
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ou 5 ?
Une solution est de réduire la largeur du processeur. Dans l'exemple précédent, il est possible de passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La solution précédente est certes très intéressante, mais elle a des défauts. Le principal est que les ports d'émission sont sous-utilisés. Et les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
===L'intérieur de l'unité d'émission===
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
==Le ''front-end'' des processeurs superscalaires==
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Le banc de registre d'un processeur superscalaire===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Une autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions auto-aligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions auto-aligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez longs, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
9w0s3y3dj2xefxkr2usasyedpmm6smi
773128
773127
2026-09-25T15:46:15Z
Mewtow
31375
/* Les processeurs historiques à double émission */
773128
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===L'impact des contraintes d’appariement sur la performance===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour la suite du chapitre===
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==Les unités de calcul d'un processeur superscalaire==
Étudier les CPU superscalaires historique était une bonne introduction propédeutique, nous allons maintenant passer aux CPU superscalaires en général. Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Et cela nous permettra de généraliser certains concepts vus sur les CPU superscalaires historiques.
Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est possible d'exploiter plusieurs unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu.
Les tout premiers CPU superscalaires étaient nombreux à utiliser cette technique. Ils étaient des CPU double émission, avec une ALU entière, une unité mémoire et une FPU. Nous parlerons de '''partitionnement INT-FLOAT-MEM'''. Il s'agit de l'HyperSPARC, de l'alpha 21064 et du PowerPC 603. Les contrainte d’appariement variaient grandement suivant le processeur. Les plus permissifs autorisaient toutes les combinaisons possibles entre : une opération entière, une opération flottante, et une unité mémoire. Mais ils n'étaient pas les seuls.
Le PowerPC 603 implémentait ce genre de double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions).
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Le '''reconditionnement d'une unité fonctionnelle''' permet à celle-ci de faire des opérations qu'elle n'est pas prévue pour. Par exemple, il est possible d'utiliser l'unité mémoire comme seconde ALU entière, ou d'utiliser la FPU comme multiplieur. Ces deux exemples ne sont pas choisit au hasard, nous les étudierons dans ce qui suit. Le reconditionnement évite d'avoir à dupliquer une unité de calcul, tout en ajoutant une voie. Il permet d'améliorer l'efficacité du partitionnement, qu'il soit utilisé seul ou non.
Il est possible de reconditionner la FPU pour faire des multiplications entières. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits. Il est possible d'utiliser de multiplieur pour faire des multiplications entières 32 bits. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
Le reconditionnement de la FPU permet d'exécuter une multiplication entière en plus des autres opérations entières. Par exemple, prenons un CPU avec double émission entière-flottante, sans multiplieur entier séparé. Le reconditionnement de la FPU permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine.
Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants.
Passons maintenant à une seconde forme de reconditionnement. Reconditionner l'unité mémoire permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, des versions améliorées de l'Intel 960. L'Intel 960 originel n'était pas superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire au monde. Il intégrait une ALU entière, une unité mémoire et une unité de branchement. Grâce au partitionnement, il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, en remplacement d'une µop mémoire. Son pipeline permettait donc les combinaisons INT + BRANCH + (MEM / + INT*).
Le cas de l'i960 nous montre que le reconditionnement permet d'améliorer l'intérêt du partitionnement. Elle permet de ne pas dupliquer d'unité de calcul, tout en permettant quand même d'émettre une seconde opération entière.
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''.
Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières ! Il doit y avoir deux ALU entières pour de la double émission entière, trois pour de triple émission éntière, 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]]
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications.
Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que dupliquer l'ALU entière peut se marier parfaitement avec la présence d'une FPU. Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU reconditionne sa FPU ou pas. Mais il est très rare qu'un CPU ait à la fois un multiplieur entier et une FPU reconditionnée.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
Le processeur SuperSPARC avait deux ALU entière et une FPU. La FPU était reconditionnée, ce qui fait que la multiplication était faite dans la FPU, idem pour la division. Il lui est possible d'émettre deux opérations entières simples, en plus d'une multiplication. Par contre, on perd la possibilité d'émettre en même temps une multiplication et une opération flottante. Les combinaisons INT + INT + (MUL/FLOAT) sont possibles pas les combinaisons INT + MUL + FPU.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires modernes utilisent toutes ces stratégies===
Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, duplication de l'ALU entière, duplication des autre unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents.
* Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU.
* Le '''reconditionnement des unités''' a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU.
* La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants.
* La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables.
De nos jours, le partitionnement est utilisé en complément de la duplication des ALU entières, et de la duplication des autres unités. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993.
Le meilleur moyen de voir comment sont combinés ces quatre technique est de regarder ce qu'il s'est passé historiquement.
==L'évolution historique de la superscalarité==
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. La plupart des CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 sont rapidement montés à de la quadruple émission, avec quelques exceptions. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 ||
|-
! 1995
| 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000
|-
! 1996
| class="f_bleu | 620 || || || || class="f_bleu | 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
===Les processeurs historiques à double émission===
Pour commencer, étudions le cas des premiers CPU superscalaires à double émission. Ils sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Notamment, la duplication des autres unités ne sert pour ainsi dire à rien, ce qui limite les possibilités, mais rendra les explications plus simples.
Il pouvaient se classer en deux/trois types principaux, assez stricts, avec quelques processeurs qui ne rentraient pas dans ces cases. Le premier utilise le partitionnement seul, le second dupliquer les ALU entières sans partitionnement, et le troisième type est volontairement passé sous silence pour le moment (nous aborderons le troisième type dans la partie sur des design à quadruple émission).
Les CPU n'utilisant que le partitionnement faisaient la double émission avec trois unités : une ALU, une FPU et une unité mémoire. Le reconditionnement de la FPU était parfois utilisé, mais l'AGU n'était jamais reconditionnée. Si la moitié des processeurs faisait du ''partitionnement complet'' entre ALU, FPU et unité mémoire, l'autre moitié utilisait une forme encore plus simple de partitionnement.
La '''double émission entière-flottante''' consiste à émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le processeur a donc deux pipelines : un pipeline entier, et un pipeline flottant. Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire, vu que les µops mémoire passent par l'ALU entière avant d’atterrir dans l'unité mémoire. Le reconditionnement de la FPU était utilisé sur les processeurs à double émission entière-flottante. Il entrainait un léger en performance, pour un cout en circuits presque nul.
{|
|[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU.]]
|[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
|}
La seconde classe de CPU superscalaires de l'époque utilisait la '''double émission entière''', à savoir qu'ils peuvent émettre une opération en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Pour cela, ils peuvent dupliquer une ALU entière, ou reconditionner l'unité mémoire. Les exemples classiques sont les processeurs Intel : i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 reconditionnait l'unité mémoire en ALU entière.
L'Intel i960 utilisait le reconditionnement de l'AGU en plus du partitionnement, ce qui lui permettait d'exécuter une seconde instruction entière, en plus d'une autre instruction. Les instructions en question étaient simples : additions, quelques décalages. De nos jours, ce combo partitionnement et reconditionnement de l'AGU n'est plus utilisé. Il n'a pas beaucoup de sens quand on peut dupliquer des ALU entières, et les processeurs récents ont un budget en transistor suffisant pour.
Le Pentium dupliquait l'ALU entière, mais n'utilisaient aucune forme de partitionnement. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Contrairement au i960, le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
Pour résumer, voici le tableau chronologique disant quel CPU utilisait quelle technique.
* Le partitionnement INT-FLOAT-MEM est en jaune.
* La double émission entière-flottante est en vert.
* La double émission entière est en rouge.
* Les CPU barrés utilisent la quadruple émission et seront vu dans la section suivante.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| class="f_vert | RS/6000 || || || class="f_rouge | i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| class="f_vert | RSC || class="f_vert | PA 7100 || SuperSPARC || || class="f_jaune | 21064 ||
|-
! 1993
| class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || ||
|-
! 1994
| class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 ||
|-
! 1995
| 603/604 || <s>PA 8000</s> || <s>UltraSPARC 1</s> || Microarchitecture P6 (+ concurrence AMD) || || <s>MIPS R1000</s>
|}
Vous voyez que certains processeur sont laissés de côté dans le tableau précédent. C'est car ils ne rentrent pas dans les catégories précédentes et faisaient autrement. Si on omet le Motorola 88110, qui utilisait une forme de double émission assez avancée, les deux CPUs qui vont nous intéresser sont le PA-RISC PA 7200 d'HP, et le SuperSPARC.
Le PA 7200, sorti en 1994, est un PA-7100 auquel on aurait rajouté une ALU entière. Précisons que ce processeur utilisait aussi le reconditionnement de la FPU, comme son prédécesseur. Les combinaisons autorisées étaient donc INT + INT, INT + MEM, INT + FLOAT/MUL, MEM + FLOAT/MUL. Les combinaisons autorisées par le partitionnement INT-FLOAT-MEM étaient donc possibles, en plus des combinaisons INT + INT, voire INT + INT + MUL (vu que la FPU est reconditionnée). Autrement dit, il pouvait faire toute ce que permettait un CPU à double émission entière, plus les combinaisons FLOAT/MUL + MEM.
Le SuperSPARC était un CPU triple émission, avec deux ALU entières. Il pouvait émettre, à chaque cycle, trois µops en piochant dans deux opérations entières, un accès mémoire, une opération flottante. Le SuperSPARC avait de nombreuses contraintes d’appariement. Par exemple, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Une bonne partie de ces contraintes était lié au banc de registre, qui n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions.
: La seconde ALU a été abandonnée dans l'HyperSPARC, qui est de plus repassé en double émission, ce qui est une évolution à rebours de l'évolution des autres CPUs. Le SuperSPARC était en avance sur son temps, ce quelques années, alors que l'HyperSPARC a un design de son temps.
===Les processeurs historiques à quadruple émission===
C'est la même année , en 1994, que la quadruple émission est arrivée et que les CPU ont commencé à avoir de plus en plus d'unités de calcul. Même les designs restés en double émission avaient ajouté des unités de calcul. Il y avait alors trois stratégies pour la quadruple émission.
La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Les FPUs étaient soit dupliquées, soit scindées en deux en séparant l'additionneur du multiplieur flottant. Le POWER 2 dupliquait ses FPUs à la perfection, alors que l'21264 partitionnait la FPU. Les accès mémoire étaient traités comme des opérations entières, et le processeur pouvait : soit en faire un seul par cycle, soit en émettre deux, sous conditions (le cache devait être double port pour lire/écrire deux données à la fois, comme on le verra dans la suite du chapitre).
Les processeurs MIPS R8000 et R10000 allaient un peu plus loin, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités.
La seconde implémentation dupliquait l'ALU entière et combinait le tout avec un partitionnement INT-FLOAT-MEM. De quoi exécuter en même temps deux opérations entières, une opération flottante, et un accès mémoire. 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).
Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, avec au minimum deux ALU entières. Pour comprendre ce que je veux dire, prenons l'exemple de l'UltraSPARC. Il avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplier flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes. Le processeur pouvait émettre 4 instructions, tant qu'elles atterrissaient dans des unités différentes.
La même méthode peut être utilisée pour la double émission. Le Motorola 88110, sorti en 1991, était un CPU à double émission avec pas moins de 10 unités : deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise.
La morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes. Le tout est résumé dans le tableau ci-dessous, avec :
* en vert les CPU utilisant la première stratégie ;
* en rouge les CPU utilisant la seconde stratégie ;
* en jaune les CPU utilisant la troisième stratégie.
{|class="wikitable"
|+ Processeurs quadruple émission historiques
|-
! Année !! Processeur !! Unités
|-
! 1991
| class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques
|-
|-
! rowspan="2" | 1994
| class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'')
|-
| class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM
|-
! rowspan="3" | 1995
| class="f_jaune | PA 8000 || class="f_jaune | 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD
|-
| class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques
|-
| MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM
|-
! rowspan="2" | 1996
| class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM
|-
| class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL)
|}
===Les processeurs superscalaires modernes===
Les CPU superscalaires modernes ont des design légèrement différents des CPU historiques. Ils dupliquent l'unité entière, mais leur partitionnement est différent. En général, les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Par exemple, un CPU à 6 voies pourra émettre/exécuter 6 opérations entières simultanément, grâce à 6 ALU entières. Mais il s'agit là d'un maximum et certaines peuvent être remplacées par une opération flottante ou mémoire, qui utilisent l'unité mémoire et/ou la FPU.
Un exemple assez parlant est les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon. C'était une microarchitecture à triple émission, avec trois ALU entières et trois unités mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements.
Il s'agit là d'une tendance, cependant. Les CPU superscalaires tendent à mettre le paquet niveau opérations arithmétiques, mais pas forcément au point de remplir la totalité de la largeur du processeur. La règle est que pour N voies, le processeur dispose d'au moins N/2 ALU entières. Attention, cela ne signifie pas que le processeur émet systématiquement N/2 opération entières. Il peut par exemple émettre une combinaison d'opérations flottantes et mémoire, avec des branchements. Et N/2 est une borne basse, la plupart des CPU x86 ont environ tournent plutôt autour des 2/3, voire des 3/4.
En général, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. La conséquence est que les unités mémoire sont dupliquées, en seconde place dans l'ordre d'importance. Les processeurs x86 ont commencé à dupliquer leurs unités mémoire dès qu'ils ont atteint la quadruple émission, pour les CPU grand public. La microarchitecture Core d'Intel utilisait la quadruple émission, avec deux unités mémoire et trois ALU entières. Les microarchitectures d'AMD K5 et K6 utilisaient aussi la quadruple émission et avaient deux ALUs pour deux unités mémoire.
Plus récemment, la microarchitecture ''Golden Cove'' dispose de cinq unités de calcul trois unités mémoire (deux spécialisées dans les écritures, trois dans les lectures). La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire.Il y a donc soit égalité, soit avantage pour les ALU entières.
Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair.
==L'unité d'émission d'un processeur superscalaire==
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Voyons pourquoi.
===L'excès de ports d'émission===
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ou 5 ?
Une solution est de réduire la largeur du processeur. Dans l'exemple précédent, il est possible de passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La solution précédente est certes très intéressante, mais elle a des défauts. Le principal est que les ports d'émission sont sous-utilisés. Et les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
===L'intérieur de l'unité d'émission===
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
==Le ''front-end'' des processeurs superscalaires==
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Le banc de registre d'un processeur superscalaire===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Une autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions auto-aligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions auto-aligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez longs, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
80j1kxeg8ukgqxbhemnvdrfx0ldscxw
773129
773128
2026-09-25T15:48:03Z
Mewtow
31375
/* Les processeurs historiques à double émission */
773129
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===L'impact des contraintes d’appariement sur la performance===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour la suite du chapitre===
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==Les unités de calcul d'un processeur superscalaire==
Étudier les CPU superscalaires historique était une bonne introduction propédeutique, nous allons maintenant passer aux CPU superscalaires en général. Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Et cela nous permettra de généraliser certains concepts vus sur les CPU superscalaires historiques.
Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est possible d'exploiter plusieurs unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu.
Les tout premiers CPU superscalaires étaient nombreux à utiliser cette technique. Ils étaient des CPU double émission, avec une ALU entière, une unité mémoire et une FPU. Nous parlerons de '''partitionnement INT-FLOAT-MEM'''. Il s'agit de l'HyperSPARC, de l'alpha 21064 et du PowerPC 603. Les contrainte d’appariement variaient grandement suivant le processeur. Les plus permissifs autorisaient toutes les combinaisons possibles entre : une opération entière, une opération flottante, et une unité mémoire. Mais ils n'étaient pas les seuls.
Le PowerPC 603 implémentait ce genre de double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions).
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Le '''reconditionnement d'une unité fonctionnelle''' permet à celle-ci de faire des opérations qu'elle n'est pas prévue pour. Par exemple, il est possible d'utiliser l'unité mémoire comme seconde ALU entière, ou d'utiliser la FPU comme multiplieur. Ces deux exemples ne sont pas choisit au hasard, nous les étudierons dans ce qui suit. Le reconditionnement évite d'avoir à dupliquer une unité de calcul, tout en ajoutant une voie. Il permet d'améliorer l'efficacité du partitionnement, qu'il soit utilisé seul ou non.
Il est possible de reconditionner la FPU pour faire des multiplications entières. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits. Il est possible d'utiliser de multiplieur pour faire des multiplications entières 32 bits. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
Le reconditionnement de la FPU permet d'exécuter une multiplication entière en plus des autres opérations entières. Par exemple, prenons un CPU avec double émission entière-flottante, sans multiplieur entier séparé. Le reconditionnement de la FPU permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine.
Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants.
Passons maintenant à une seconde forme de reconditionnement. Reconditionner l'unité mémoire permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, des versions améliorées de l'Intel 960. L'Intel 960 originel n'était pas superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire au monde. Il intégrait une ALU entière, une unité mémoire et une unité de branchement. Grâce au partitionnement, il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, en remplacement d'une µop mémoire. Son pipeline permettait donc les combinaisons INT + BRANCH + (MEM / + INT*).
Le cas de l'i960 nous montre que le reconditionnement permet d'améliorer l'intérêt du partitionnement. Elle permet de ne pas dupliquer d'unité de calcul, tout en permettant quand même d'émettre une seconde opération entière.
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''.
Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières ! Il doit y avoir deux ALU entières pour de la double émission entière, trois pour de triple émission éntière, 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]]
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications.
Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que dupliquer l'ALU entière peut se marier parfaitement avec la présence d'une FPU. Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU reconditionne sa FPU ou pas. Mais il est très rare qu'un CPU ait à la fois un multiplieur entier et une FPU reconditionnée.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
Le processeur SuperSPARC avait deux ALU entière et une FPU. La FPU était reconditionnée, ce qui fait que la multiplication était faite dans la FPU, idem pour la division. Il lui est possible d'émettre deux opérations entières simples, en plus d'une multiplication. Par contre, on perd la possibilité d'émettre en même temps une multiplication et une opération flottante. Les combinaisons INT + INT + (MUL/FLOAT) sont possibles pas les combinaisons INT + MUL + FPU.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires modernes utilisent toutes ces stratégies===
Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, duplication de l'ALU entière, duplication des autre unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents.
* Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU.
* Le '''reconditionnement des unités''' a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU.
* La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants.
* La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables.
De nos jours, le partitionnement est utilisé en complément de la duplication des ALU entières, et de la duplication des autres unités. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993.
Le meilleur moyen de voir comment sont combinés ces quatre technique est de regarder ce qu'il s'est passé historiquement.
==L'évolution historique de la superscalarité==
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. La plupart des CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 sont rapidement montés à de la quadruple émission, avec quelques exceptions. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 ||
|-
! 1995
| 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000
|-
! 1996
| class="f_bleu | 620 || || || || class="f_bleu | 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
===Les processeurs historiques à double émission===
Pour commencer, étudions le cas des premiers CPU superscalaires à double émission. Ils sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Notamment, la duplication des autres unités ne sert pour ainsi dire à rien, ce qui limite les possibilités, mais rendra les explications plus simples.
Il pouvaient se classer en deux/trois types principaux, assez stricts, avec quelques processeurs qui ne rentraient pas dans ces cases. Le premier utilise le partitionnement seul, le second dupliquer les ALU entières sans partitionnement, et le troisième type est volontairement passé sous silence pour le moment (nous aborderons le troisième type dans la partie sur des design à quadruple émission).
Les CPU n'utilisant que le partitionnement faisaient la double émission avec trois unités : une ALU, une FPU et une unité mémoire. Le reconditionnement de la FPU était parfois utilisé, mais l'AGU n'était jamais reconditionnée. Si la moitié des processeurs faisait du ''partitionnement complet'' entre ALU, FPU et unité mémoire, l'autre moitié utilisait une forme encore plus simple de partitionnement.
La '''double émission entière-flottante''' consiste à émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le processeur a donc deux pipelines : un pipeline entier, et un pipeline flottant. Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire, vu que les µops mémoire passent par l'ALU entière avant d’atterrir dans l'unité mémoire. Le reconditionnement de la FPU était utilisé sur les processeurs à double émission entière-flottante. Il entrainait un léger en performance, pour un cout en circuits presque nul.
{|
|[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU.]]
|[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
|}
La seconde classe de CPU superscalaires de l'époque utilisait la '''double émission entière''', à savoir qu'ils peuvent émettre une opération en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Pour cela, ils peuvent dupliquer une ALU entière, ou reconditionner l'unité mémoire. Les exemples classiques sont les processeurs Intel : i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 reconditionnait l'unité mémoire en ALU entière.
L'Intel i960 utilisait le reconditionnement de l'AGU en plus du partitionnement, ce qui lui permettait d'exécuter une seconde instruction entière, en plus d'une autre instruction. Les instructions en question étaient simples : additions, quelques décalages. De nos jours, ce combo partitionnement et reconditionnement de l'AGU n'est plus utilisé. Il n'a pas beaucoup de sens quand on peut dupliquer des ALU entières, et les processeurs récents ont un budget en transistor suffisant pour.
Le Pentium dupliquait l'ALU entière, mais n'utilisaient aucune forme de partitionnement. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Contrairement au i960, le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
Pour résumer, voici le tableau chronologique disant quel CPU utilisait quelle technique.
* Le partitionnement INT-FLOAT-MEM est en jaune.
* La double émission entière-flottante est en vert.
* La double émission entière est en rouge.
* Les CPU barrés utilisent la quadruple émission et seront vu dans la section suivante.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| class="f_vert | RS/6000 || || || class="f_rouge | i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| class="f_vert | RSC || class="f_vert | PA 7100 || SuperSPARC || || class="f_jaune | 21064 ||
|-
! 1993
| class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || ||
|-
! 1994
| <s>POWER 2</s> || PA 7200 || || || <s>21164</s> ||
|}
Vous voyez que certains processeur sont laissés de côté dans le tableau précédent. C'est car ils ne rentrent pas dans les catégories précédentes et faisaient autrement. Si on omet le Motorola 88110, qui utilisait une forme de double émission assez avancée, les deux CPUs qui vont nous intéresser sont le PA-RISC PA 7200 d'HP, et le SuperSPARC.
Le PA 7200, sorti en 1994, est un PA-7100 auquel on aurait rajouté une ALU entière. Précisons que ce processeur utilisait aussi le reconditionnement de la FPU, comme son prédécesseur. Les combinaisons autorisées étaient donc INT + INT, INT + MEM, INT + FLOAT/MUL, MEM + FLOAT/MUL. Les combinaisons autorisées par le partitionnement INT-FLOAT-MEM étaient donc possibles, en plus des combinaisons INT + INT, voire INT + INT + MUL (vu que la FPU est reconditionnée). Autrement dit, il pouvait faire toute ce que permettait un CPU à double émission entière, plus les combinaisons FLOAT/MUL + MEM.
Le SuperSPARC était un CPU triple émission, avec deux ALU entières. Il pouvait émettre, à chaque cycle, trois µops en piochant dans deux opérations entières, un accès mémoire, une opération flottante. Le SuperSPARC avait de nombreuses contraintes d’appariement. Par exemple, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Une bonne partie de ces contraintes était lié au banc de registre, qui n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions.
: La seconde ALU a été abandonnée dans l'HyperSPARC, qui est de plus repassé en double émission, ce qui est une évolution à rebours de l'évolution des autres CPUs. Le SuperSPARC était en avance sur son temps, ce quelques années, alors que l'HyperSPARC a un design de son temps.
===Les processeurs historiques à quadruple émission===
C'est la même année , en 1994, que la quadruple émission est arrivée et que les CPU ont commencé à avoir de plus en plus d'unités de calcul. Même les designs restés en double émission avaient ajouté des unités de calcul. Il y avait alors trois stratégies pour la quadruple émission.
La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Les FPUs étaient soit dupliquées, soit scindées en deux en séparant l'additionneur du multiplieur flottant. Le POWER 2 dupliquait ses FPUs à la perfection, alors que l'21264 partitionnait la FPU. Les accès mémoire étaient traités comme des opérations entières, et le processeur pouvait : soit en faire un seul par cycle, soit en émettre deux, sous conditions (le cache devait être double port pour lire/écrire deux données à la fois, comme on le verra dans la suite du chapitre).
Les processeurs MIPS R8000 et R10000 allaient un peu plus loin, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités.
La seconde implémentation dupliquait l'ALU entière et combinait le tout avec un partitionnement INT-FLOAT-MEM. De quoi exécuter en même temps deux opérations entières, une opération flottante, et un accès mémoire. 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).
Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, avec au minimum deux ALU entières. Pour comprendre ce que je veux dire, prenons l'exemple de l'UltraSPARC. Il avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplier flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes. Le processeur pouvait émettre 4 instructions, tant qu'elles atterrissaient dans des unités différentes.
La même méthode peut être utilisée pour la double émission. Le Motorola 88110, sorti en 1991, était un CPU à double émission avec pas moins de 10 unités : deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise.
La morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes. Le tout est résumé dans le tableau ci-dessous, avec :
* en vert les CPU utilisant la première stratégie ;
* en rouge les CPU utilisant la seconde stratégie ;
* en jaune les CPU utilisant la troisième stratégie.
{|class="wikitable"
|+ Processeurs quadruple émission historiques
|-
! Année !! Processeur !! Unités
|-
! 1991
| class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques
|-
|-
! rowspan="2" | 1994
| class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'')
|-
| class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM
|-
! rowspan="3" | 1995
| class="f_jaune | PA 8000 || class="f_jaune | 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD
|-
| class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques
|-
| MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM
|-
! rowspan="2" | 1996
| class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM
|-
| class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL)
|}
===Les processeurs superscalaires modernes===
Les CPU superscalaires modernes ont des design légèrement différents des CPU historiques. Ils dupliquent l'unité entière, mais leur partitionnement est différent. En général, les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Par exemple, un CPU à 6 voies pourra émettre/exécuter 6 opérations entières simultanément, grâce à 6 ALU entières. Mais il s'agit là d'un maximum et certaines peuvent être remplacées par une opération flottante ou mémoire, qui utilisent l'unité mémoire et/ou la FPU.
Un exemple assez parlant est les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon. C'était une microarchitecture à triple émission, avec trois ALU entières et trois unités mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements.
Il s'agit là d'une tendance, cependant. Les CPU superscalaires tendent à mettre le paquet niveau opérations arithmétiques, mais pas forcément au point de remplir la totalité de la largeur du processeur. La règle est que pour N voies, le processeur dispose d'au moins N/2 ALU entières. Attention, cela ne signifie pas que le processeur émet systématiquement N/2 opération entières. Il peut par exemple émettre une combinaison d'opérations flottantes et mémoire, avec des branchements. Et N/2 est une borne basse, la plupart des CPU x86 ont environ tournent plutôt autour des 2/3, voire des 3/4.
En général, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. La conséquence est que les unités mémoire sont dupliquées, en seconde place dans l'ordre d'importance. Les processeurs x86 ont commencé à dupliquer leurs unités mémoire dès qu'ils ont atteint la quadruple émission, pour les CPU grand public. La microarchitecture Core d'Intel utilisait la quadruple émission, avec deux unités mémoire et trois ALU entières. Les microarchitectures d'AMD K5 et K6 utilisaient aussi la quadruple émission et avaient deux ALUs pour deux unités mémoire.
Plus récemment, la microarchitecture ''Golden Cove'' dispose de cinq unités de calcul trois unités mémoire (deux spécialisées dans les écritures, trois dans les lectures). La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire.Il y a donc soit égalité, soit avantage pour les ALU entières.
Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair.
==L'unité d'émission d'un processeur superscalaire==
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Voyons pourquoi.
===L'excès de ports d'émission===
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ou 5 ?
Une solution est de réduire la largeur du processeur. Dans l'exemple précédent, il est possible de passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La solution précédente est certes très intéressante, mais elle a des défauts. Le principal est que les ports d'émission sont sous-utilisés. Et les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
===L'intérieur de l'unité d'émission===
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
==Le ''front-end'' des processeurs superscalaires==
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Le banc de registre d'un processeur superscalaire===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Une autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions auto-aligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions auto-aligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez longs, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
fja9yxphp61zvqdc6z22e0zld2g7xht
773130
773129
2026-09-25T15:50:38Z
Mewtow
31375
/* Les unités de calcul d'un processeur superscalaire */
773130
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===L'impact des contraintes d’appariement sur la performance===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour la suite du chapitre===
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==Les unités de calcul d'un processeur superscalaire==
Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est possible d'exploiter plusieurs unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu.
Les tout premiers CPU superscalaires étaient nombreux à utiliser cette technique. Ils étaient des CPU double émission, avec une ALU entière, une unité mémoire et une FPU. Nous parlerons de '''partitionnement INT-FLOAT-MEM'''. Il s'agit de l'HyperSPARC, de l'alpha 21064 et du PowerPC 603. Les contrainte d’appariement variaient grandement suivant le processeur. Les plus permissifs autorisaient toutes les combinaisons possibles entre : une opération entière, une opération flottante, et une unité mémoire. Mais ils n'étaient pas les seuls.
Le PowerPC 603 implémentait ce genre de double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions).
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Le '''reconditionnement d'une unité fonctionnelle''' permet à celle-ci de faire des opérations qu'elle n'est pas prévue pour. Par exemple, il est possible d'utiliser l'unité mémoire comme seconde ALU entière, ou d'utiliser la FPU comme multiplieur. Ces deux exemples ne sont pas choisit au hasard, nous les étudierons dans ce qui suit. Le reconditionnement évite d'avoir à dupliquer une unité de calcul, tout en ajoutant une voie. Il permet d'améliorer l'efficacité du partitionnement, qu'il soit utilisé seul ou non.
Il est possible de reconditionner la FPU pour faire des multiplications entières. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits. Il est possible d'utiliser de multiplieur pour faire des multiplications entières 32 bits. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
Le reconditionnement de la FPU permet d'exécuter une multiplication entière en plus des autres opérations entières. Par exemple, prenons un CPU avec double émission entière-flottante, sans multiplieur entier séparé. Le reconditionnement de la FPU permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine.
Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants.
Passons maintenant à une seconde forme de reconditionnement. Reconditionner l'unité mémoire permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, des versions améliorées de l'Intel 960. L'Intel 960 originel n'était pas superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire au monde. Il intégrait une ALU entière, une unité mémoire et une unité de branchement. Grâce au partitionnement, il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, en remplacement d'une µop mémoire. Son pipeline permettait donc les combinaisons INT + BRANCH + (MEM / + INT*).
Le cas de l'i960 nous montre que le reconditionnement permet d'améliorer l'intérêt du partitionnement. Elle permet de ne pas dupliquer d'unité de calcul, tout en permettant quand même d'émettre une seconde opération entière.
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''.
Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières ! Il doit y avoir deux ALU entières pour de la double émission entière, trois pour de triple émission éntière, 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]]
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications.
Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que dupliquer l'ALU entière peut se marier parfaitement avec la présence d'une FPU. Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU reconditionne sa FPU ou pas. Mais il est très rare qu'un CPU ait à la fois un multiplieur entier et une FPU reconditionnée.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
Le processeur SuperSPARC avait deux ALU entière et une FPU. La FPU était reconditionnée, ce qui fait que la multiplication était faite dans la FPU, idem pour la division. Il lui est possible d'émettre deux opérations entières simples, en plus d'une multiplication. Par contre, on perd la possibilité d'émettre en même temps une multiplication et une opération flottante. Les combinaisons INT + INT + (MUL/FLOAT) sont possibles pas les combinaisons INT + MUL + FPU.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires modernes utilisent toutes ces stratégies===
Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, duplication de l'ALU entière, duplication des autre unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents.
* Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU.
* Le '''reconditionnement des unités''' a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU.
* La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants.
* La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables.
De nos jours, le partitionnement est utilisé en complément de la duplication des ALU entières, et de la duplication des autres unités. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993.
Le meilleur moyen de voir comment sont combinés ces quatre technique est de regarder ce qu'il s'est passé historiquement.
==L'évolution historique de la superscalarité==
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. La plupart des CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 sont rapidement montés à de la quadruple émission, avec quelques exceptions. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 ||
|-
! 1995
| 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000
|-
! 1996
| class="f_bleu | 620 || || || || class="f_bleu | 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
===Les processeurs historiques à double émission===
Pour commencer, étudions le cas des premiers CPU superscalaires à double émission. Ils sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Notamment, la duplication des autres unités ne sert pour ainsi dire à rien, ce qui limite les possibilités, mais rendra les explications plus simples.
Il pouvaient se classer en deux/trois types principaux, assez stricts, avec quelques processeurs qui ne rentraient pas dans ces cases. Le premier utilise le partitionnement seul, le second dupliquer les ALU entières sans partitionnement, et le troisième type est volontairement passé sous silence pour le moment (nous aborderons le troisième type dans la partie sur des design à quadruple émission).
Les CPU n'utilisant que le partitionnement faisaient la double émission avec trois unités : une ALU, une FPU et une unité mémoire. Le reconditionnement de la FPU était parfois utilisé, mais l'AGU n'était jamais reconditionnée. Si la moitié des processeurs faisait du ''partitionnement complet'' entre ALU, FPU et unité mémoire, l'autre moitié utilisait une forme encore plus simple de partitionnement.
La '''double émission entière-flottante''' consiste à émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le processeur a donc deux pipelines : un pipeline entier, et un pipeline flottant. Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire, vu que les µops mémoire passent par l'ALU entière avant d’atterrir dans l'unité mémoire. Le reconditionnement de la FPU était utilisé sur les processeurs à double émission entière-flottante. Il entrainait un léger en performance, pour un cout en circuits presque nul.
{|
|[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU.]]
|[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
|}
La seconde classe de CPU superscalaires de l'époque utilisait la '''double émission entière''', à savoir qu'ils peuvent émettre une opération en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Pour cela, ils peuvent dupliquer une ALU entière, ou reconditionner l'unité mémoire. Les exemples classiques sont les processeurs Intel : i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 reconditionnait l'unité mémoire en ALU entière.
L'Intel i960 utilisait le reconditionnement de l'AGU en plus du partitionnement, ce qui lui permettait d'exécuter une seconde instruction entière, en plus d'une autre instruction. Les instructions en question étaient simples : additions, quelques décalages. De nos jours, ce combo partitionnement et reconditionnement de l'AGU n'est plus utilisé. Il n'a pas beaucoup de sens quand on peut dupliquer des ALU entières, et les processeurs récents ont un budget en transistor suffisant pour.
Le Pentium dupliquait l'ALU entière, mais n'utilisaient aucune forme de partitionnement. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Contrairement au i960, le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
Pour résumer, voici le tableau chronologique disant quel CPU utilisait quelle technique.
* Le partitionnement INT-FLOAT-MEM est en jaune.
* La double émission entière-flottante est en vert.
* La double émission entière est en rouge.
* Les CPU barrés utilisent la quadruple émission et seront vu dans la section suivante.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| class="f_vert | RS/6000 || || || class="f_rouge | i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| class="f_vert | RSC || class="f_vert | PA 7100 || SuperSPARC || || class="f_jaune | 21064 ||
|-
! 1993
| class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || ||
|-
! 1994
| <s>POWER 2</s> || PA 7200 || || || <s>21164</s> ||
|}
Vous voyez que certains processeur sont laissés de côté dans le tableau précédent. C'est car ils ne rentrent pas dans les catégories précédentes et faisaient autrement. Si on omet le Motorola 88110, qui utilisait une forme de double émission assez avancée, les deux CPUs qui vont nous intéresser sont le PA-RISC PA 7200 d'HP, et le SuperSPARC.
Le PA 7200, sorti en 1994, est un PA-7100 auquel on aurait rajouté une ALU entière. Précisons que ce processeur utilisait aussi le reconditionnement de la FPU, comme son prédécesseur. Les combinaisons autorisées étaient donc INT + INT, INT + MEM, INT + FLOAT/MUL, MEM + FLOAT/MUL. Les combinaisons autorisées par le partitionnement INT-FLOAT-MEM étaient donc possibles, en plus des combinaisons INT + INT, voire INT + INT + MUL (vu que la FPU est reconditionnée). Autrement dit, il pouvait faire toute ce que permettait un CPU à double émission entière, plus les combinaisons FLOAT/MUL + MEM.
Le SuperSPARC était un CPU triple émission, avec deux ALU entières. Il pouvait émettre, à chaque cycle, trois µops en piochant dans deux opérations entières, un accès mémoire, une opération flottante. Le SuperSPARC avait de nombreuses contraintes d’appariement. Par exemple, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Une bonne partie de ces contraintes était lié au banc de registre, qui n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions.
: La seconde ALU a été abandonnée dans l'HyperSPARC, qui est de plus repassé en double émission, ce qui est une évolution à rebours de l'évolution des autres CPUs. Le SuperSPARC était en avance sur son temps, ce quelques années, alors que l'HyperSPARC a un design de son temps.
===Les processeurs historiques à quadruple émission===
C'est la même année , en 1994, que la quadruple émission est arrivée et que les CPU ont commencé à avoir de plus en plus d'unités de calcul. Même les designs restés en double émission avaient ajouté des unités de calcul. Il y avait alors trois stratégies pour la quadruple émission.
La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Les FPUs étaient soit dupliquées, soit scindées en deux en séparant l'additionneur du multiplieur flottant. Le POWER 2 dupliquait ses FPUs à la perfection, alors que l'21264 partitionnait la FPU. Les accès mémoire étaient traités comme des opérations entières, et le processeur pouvait : soit en faire un seul par cycle, soit en émettre deux, sous conditions (le cache devait être double port pour lire/écrire deux données à la fois, comme on le verra dans la suite du chapitre).
Les processeurs MIPS R8000 et R10000 allaient un peu plus loin, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités.
La seconde implémentation dupliquait l'ALU entière et combinait le tout avec un partitionnement INT-FLOAT-MEM. De quoi exécuter en même temps deux opérations entières, une opération flottante, et un accès mémoire. 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).
Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, avec au minimum deux ALU entières. Pour comprendre ce que je veux dire, prenons l'exemple de l'UltraSPARC. Il avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplier flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes. Le processeur pouvait émettre 4 instructions, tant qu'elles atterrissaient dans des unités différentes.
La même méthode peut être utilisée pour la double émission. Le Motorola 88110, sorti en 1991, était un CPU à double émission avec pas moins de 10 unités : deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise.
La morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes. Le tout est résumé dans le tableau ci-dessous, avec :
* en vert les CPU utilisant la première stratégie ;
* en rouge les CPU utilisant la seconde stratégie ;
* en jaune les CPU utilisant la troisième stratégie.
{|class="wikitable"
|+ Processeurs quadruple émission historiques
|-
! Année !! Processeur !! Unités
|-
! 1991
| class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques
|-
|-
! rowspan="2" | 1994
| class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'')
|-
| class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM
|-
! rowspan="3" | 1995
| class="f_jaune | PA 8000 || class="f_jaune | 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD
|-
| class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques
|-
| MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM
|-
! rowspan="2" | 1996
| class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM
|-
| class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL)
|}
===Les processeurs superscalaires modernes===
Les CPU superscalaires modernes ont des design légèrement différents des CPU historiques. Ils dupliquent l'unité entière, mais leur partitionnement est différent. En général, les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Par exemple, un CPU à 6 voies pourra émettre/exécuter 6 opérations entières simultanément, grâce à 6 ALU entières. Mais il s'agit là d'un maximum et certaines peuvent être remplacées par une opération flottante ou mémoire, qui utilisent l'unité mémoire et/ou la FPU.
Un exemple assez parlant est les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon. C'était une microarchitecture à triple émission, avec trois ALU entières et trois unités mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements.
Il s'agit là d'une tendance, cependant. Les CPU superscalaires tendent à mettre le paquet niveau opérations arithmétiques, mais pas forcément au point de remplir la totalité de la largeur du processeur. La règle est que pour N voies, le processeur dispose d'au moins N/2 ALU entières. Attention, cela ne signifie pas que le processeur émet systématiquement N/2 opération entières. Il peut par exemple émettre une combinaison d'opérations flottantes et mémoire, avec des branchements. Et N/2 est une borne basse, la plupart des CPU x86 ont environ tournent plutôt autour des 2/3, voire des 3/4.
En général, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. La conséquence est que les unités mémoire sont dupliquées, en seconde place dans l'ordre d'importance. Les processeurs x86 ont commencé à dupliquer leurs unités mémoire dès qu'ils ont atteint la quadruple émission, pour les CPU grand public. La microarchitecture Core d'Intel utilisait la quadruple émission, avec deux unités mémoire et trois ALU entières. Les microarchitectures d'AMD K5 et K6 utilisaient aussi la quadruple émission et avaient deux ALUs pour deux unités mémoire.
Plus récemment, la microarchitecture ''Golden Cove'' dispose de cinq unités de calcul trois unités mémoire (deux spécialisées dans les écritures, trois dans les lectures). La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire.Il y a donc soit égalité, soit avantage pour les ALU entières.
Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair.
==L'unité d'émission d'un processeur superscalaire==
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Voyons pourquoi.
===L'excès de ports d'émission===
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ou 5 ?
Une solution est de réduire la largeur du processeur. Dans l'exemple précédent, il est possible de passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La solution précédente est certes très intéressante, mais elle a des défauts. Le principal est que les ports d'émission sont sous-utilisés. Et les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
===L'intérieur de l'unité d'émission===
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
==Le ''front-end'' des processeurs superscalaires==
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Le banc de registre d'un processeur superscalaire===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Une autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions auto-aligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions auto-aligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez longs, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
gfrsaslm27yy9c3y5g2vs2fv3tgvubq
773131
773130
2026-09-25T15:51:26Z
Mewtow
31375
/* Les unités de calcul d'un processeur superscalaire */
773131
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===L'impact des contraintes d’appariement sur la performance===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour la suite du chapitre===
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==Les unités de calcul d'un processeur superscalaire==
Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. Il y a aussi plusieurs décodeur, pour décoder les instructions chargées. Après tout, si le CPU charge 4 instructions, cela demande de décoder 4 instructions, donc 4 décodeurs. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est possible d'exploiter plusieurs unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu.
Les tout premiers CPU superscalaires étaient nombreux à utiliser cette technique. Ils étaient des CPU double émission, avec une ALU entière, une unité mémoire et une FPU. Nous parlerons de '''partitionnement INT-FLOAT-MEM'''. Il s'agit de l'HyperSPARC, de l'alpha 21064 et du PowerPC 603. Les contrainte d’appariement variaient grandement suivant le processeur. Les plus permissifs autorisaient toutes les combinaisons possibles entre : une opération entière, une opération flottante, et une unité mémoire. Mais ils n'étaient pas les seuls.
Le PowerPC 603 implémentait ce genre de double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions).
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Le '''reconditionnement d'une unité fonctionnelle''' permet à celle-ci de faire des opérations qu'elle n'est pas prévue pour. Par exemple, il est possible d'utiliser l'unité mémoire comme seconde ALU entière, ou d'utiliser la FPU comme multiplieur. Ces deux exemples ne sont pas choisit au hasard, nous les étudierons dans ce qui suit. Le reconditionnement évite d'avoir à dupliquer une unité de calcul, tout en ajoutant une voie. Il permet d'améliorer l'efficacité du partitionnement, qu'il soit utilisé seul ou non.
Il est possible de reconditionner la FPU pour faire des multiplications entières. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits. Il est possible d'utiliser de multiplieur pour faire des multiplications entières 32 bits. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
Le reconditionnement de la FPU permet d'exécuter une multiplication entière en plus des autres opérations entières. Par exemple, prenons un CPU avec double émission entière-flottante, sans multiplieur entier séparé. Le reconditionnement de la FPU permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine.
Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants.
Passons maintenant à une seconde forme de reconditionnement. Reconditionner l'unité mémoire permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, des versions améliorées de l'Intel 960. L'Intel 960 originel n'était pas superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire au monde. Il intégrait une ALU entière, une unité mémoire et une unité de branchement. Grâce au partitionnement, il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, en remplacement d'une µop mémoire. Son pipeline permettait donc les combinaisons INT + BRANCH + (MEM / + INT*).
Le cas de l'i960 nous montre que le reconditionnement permet d'améliorer l'intérêt du partitionnement. Elle permet de ne pas dupliquer d'unité de calcul, tout en permettant quand même d'émettre une seconde opération entière.
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''.
Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières ! Il doit y avoir deux ALU entières pour de la double émission entière, trois pour de triple émission éntière, 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]]
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications.
Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que dupliquer l'ALU entière peut se marier parfaitement avec la présence d'une FPU. Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU reconditionne sa FPU ou pas. Mais il est très rare qu'un CPU ait à la fois un multiplieur entier et une FPU reconditionnée.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
Le processeur SuperSPARC avait deux ALU entière et une FPU. La FPU était reconditionnée, ce qui fait que la multiplication était faite dans la FPU, idem pour la division. Il lui est possible d'émettre deux opérations entières simples, en plus d'une multiplication. Par contre, on perd la possibilité d'émettre en même temps une multiplication et une opération flottante. Les combinaisons INT + INT + (MUL/FLOAT) sont possibles pas les combinaisons INT + MUL + FPU.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires modernes utilisent toutes ces stratégies===
Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, duplication de l'ALU entière, duplication des autre unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents.
* Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU.
* Le '''reconditionnement des unités''' a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU.
* La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants.
* La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables.
De nos jours, le partitionnement est utilisé en complément de la duplication des ALU entières, et de la duplication des autres unités. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993.
Le meilleur moyen de voir comment sont combinés ces quatre technique est de regarder ce qu'il s'est passé historiquement.
==L'évolution historique de la superscalarité==
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. La plupart des CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 sont rapidement montés à de la quadruple émission, avec quelques exceptions. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 ||
|-
! 1995
| 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000
|-
! 1996
| class="f_bleu | 620 || || || || class="f_bleu | 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
===Les processeurs historiques à double émission===
Pour commencer, étudions le cas des premiers CPU superscalaires à double émission. Ils sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Notamment, la duplication des autres unités ne sert pour ainsi dire à rien, ce qui limite les possibilités, mais rendra les explications plus simples.
Il pouvaient se classer en deux/trois types principaux, assez stricts, avec quelques processeurs qui ne rentraient pas dans ces cases. Le premier utilise le partitionnement seul, le second dupliquer les ALU entières sans partitionnement, et le troisième type est volontairement passé sous silence pour le moment (nous aborderons le troisième type dans la partie sur des design à quadruple émission).
Les CPU n'utilisant que le partitionnement faisaient la double émission avec trois unités : une ALU, une FPU et une unité mémoire. Le reconditionnement de la FPU était parfois utilisé, mais l'AGU n'était jamais reconditionnée. Si la moitié des processeurs faisait du ''partitionnement complet'' entre ALU, FPU et unité mémoire, l'autre moitié utilisait une forme encore plus simple de partitionnement.
La '''double émission entière-flottante''' consiste à émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le processeur a donc deux pipelines : un pipeline entier, et un pipeline flottant. Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire, vu que les µops mémoire passent par l'ALU entière avant d’atterrir dans l'unité mémoire. Le reconditionnement de la FPU était utilisé sur les processeurs à double émission entière-flottante. Il entrainait un léger en performance, pour un cout en circuits presque nul.
{|
|[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU.]]
|[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
|}
La seconde classe de CPU superscalaires de l'époque utilisait la '''double émission entière''', à savoir qu'ils peuvent émettre une opération en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Pour cela, ils peuvent dupliquer une ALU entière, ou reconditionner l'unité mémoire. Les exemples classiques sont les processeurs Intel : i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 reconditionnait l'unité mémoire en ALU entière.
L'Intel i960 utilisait le reconditionnement de l'AGU en plus du partitionnement, ce qui lui permettait d'exécuter une seconde instruction entière, en plus d'une autre instruction. Les instructions en question étaient simples : additions, quelques décalages. De nos jours, ce combo partitionnement et reconditionnement de l'AGU n'est plus utilisé. Il n'a pas beaucoup de sens quand on peut dupliquer des ALU entières, et les processeurs récents ont un budget en transistor suffisant pour.
Le Pentium dupliquait l'ALU entière, mais n'utilisaient aucune forme de partitionnement. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Contrairement au i960, le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
Pour résumer, voici le tableau chronologique disant quel CPU utilisait quelle technique.
* Le partitionnement INT-FLOAT-MEM est en jaune.
* La double émission entière-flottante est en vert.
* La double émission entière est en rouge.
* Les CPU barrés utilisent la quadruple émission et seront vu dans la section suivante.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| class="f_vert | RS/6000 || || || class="f_rouge | i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| class="f_vert | RSC || class="f_vert | PA 7100 || SuperSPARC || || class="f_jaune | 21064 ||
|-
! 1993
| class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || ||
|-
! 1994
| <s>POWER 2</s> || PA 7200 || || || <s>21164</s> ||
|}
Vous voyez que certains processeur sont laissés de côté dans le tableau précédent. C'est car ils ne rentrent pas dans les catégories précédentes et faisaient autrement. Si on omet le Motorola 88110, qui utilisait une forme de double émission assez avancée, les deux CPUs qui vont nous intéresser sont le PA-RISC PA 7200 d'HP, et le SuperSPARC.
Le PA 7200, sorti en 1994, est un PA-7100 auquel on aurait rajouté une ALU entière. Précisons que ce processeur utilisait aussi le reconditionnement de la FPU, comme son prédécesseur. Les combinaisons autorisées étaient donc INT + INT, INT + MEM, INT + FLOAT/MUL, MEM + FLOAT/MUL. Les combinaisons autorisées par le partitionnement INT-FLOAT-MEM étaient donc possibles, en plus des combinaisons INT + INT, voire INT + INT + MUL (vu que la FPU est reconditionnée). Autrement dit, il pouvait faire toute ce que permettait un CPU à double émission entière, plus les combinaisons FLOAT/MUL + MEM.
Le SuperSPARC était un CPU triple émission, avec deux ALU entières. Il pouvait émettre, à chaque cycle, trois µops en piochant dans deux opérations entières, un accès mémoire, une opération flottante. Le SuperSPARC avait de nombreuses contraintes d’appariement. Par exemple, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Une bonne partie de ces contraintes était lié au banc de registre, qui n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions.
: La seconde ALU a été abandonnée dans l'HyperSPARC, qui est de plus repassé en double émission, ce qui est une évolution à rebours de l'évolution des autres CPUs. Le SuperSPARC était en avance sur son temps, ce quelques années, alors que l'HyperSPARC a un design de son temps.
===Les processeurs historiques à quadruple émission===
C'est la même année , en 1994, que la quadruple émission est arrivée et que les CPU ont commencé à avoir de plus en plus d'unités de calcul. Même les designs restés en double émission avaient ajouté des unités de calcul. Il y avait alors trois stratégies pour la quadruple émission.
La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Les FPUs étaient soit dupliquées, soit scindées en deux en séparant l'additionneur du multiplieur flottant. Le POWER 2 dupliquait ses FPUs à la perfection, alors que l'21264 partitionnait la FPU. Les accès mémoire étaient traités comme des opérations entières, et le processeur pouvait : soit en faire un seul par cycle, soit en émettre deux, sous conditions (le cache devait être double port pour lire/écrire deux données à la fois, comme on le verra dans la suite du chapitre).
Les processeurs MIPS R8000 et R10000 allaient un peu plus loin, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités.
La seconde implémentation dupliquait l'ALU entière et combinait le tout avec un partitionnement INT-FLOAT-MEM. De quoi exécuter en même temps deux opérations entières, une opération flottante, et un accès mémoire. 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).
Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, avec au minimum deux ALU entières. Pour comprendre ce que je veux dire, prenons l'exemple de l'UltraSPARC. Il avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplier flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes. Le processeur pouvait émettre 4 instructions, tant qu'elles atterrissaient dans des unités différentes.
La même méthode peut être utilisée pour la double émission. Le Motorola 88110, sorti en 1991, était un CPU à double émission avec pas moins de 10 unités : deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise.
La morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes. Le tout est résumé dans le tableau ci-dessous, avec :
* en vert les CPU utilisant la première stratégie ;
* en rouge les CPU utilisant la seconde stratégie ;
* en jaune les CPU utilisant la troisième stratégie.
{|class="wikitable"
|+ Processeurs quadruple émission historiques
|-
! Année !! Processeur !! Unités
|-
! 1991
| class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques
|-
|-
! rowspan="2" | 1994
| class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'')
|-
| class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM
|-
! rowspan="3" | 1995
| class="f_jaune | PA 8000 || class="f_jaune | 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD
|-
| class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques
|-
| MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM
|-
! rowspan="2" | 1996
| class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM
|-
| class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL)
|}
===Les processeurs superscalaires modernes===
Les CPU superscalaires modernes ont des design légèrement différents des CPU historiques. Ils dupliquent l'unité entière, mais leur partitionnement est différent. En général, les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Par exemple, un CPU à 6 voies pourra émettre/exécuter 6 opérations entières simultanément, grâce à 6 ALU entières. Mais il s'agit là d'un maximum et certaines peuvent être remplacées par une opération flottante ou mémoire, qui utilisent l'unité mémoire et/ou la FPU.
Un exemple assez parlant est les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon. C'était une microarchitecture à triple émission, avec trois ALU entières et trois unités mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements.
Il s'agit là d'une tendance, cependant. Les CPU superscalaires tendent à mettre le paquet niveau opérations arithmétiques, mais pas forcément au point de remplir la totalité de la largeur du processeur. La règle est que pour N voies, le processeur dispose d'au moins N/2 ALU entières. Attention, cela ne signifie pas que le processeur émet systématiquement N/2 opération entières. Il peut par exemple émettre une combinaison d'opérations flottantes et mémoire, avec des branchements. Et N/2 est une borne basse, la plupart des CPU x86 ont environ tournent plutôt autour des 2/3, voire des 3/4.
En général, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. La conséquence est que les unités mémoire sont dupliquées, en seconde place dans l'ordre d'importance. Les processeurs x86 ont commencé à dupliquer leurs unités mémoire dès qu'ils ont atteint la quadruple émission, pour les CPU grand public. La microarchitecture Core d'Intel utilisait la quadruple émission, avec deux unités mémoire et trois ALU entières. Les microarchitectures d'AMD K5 et K6 utilisaient aussi la quadruple émission et avaient deux ALUs pour deux unités mémoire.
Plus récemment, la microarchitecture ''Golden Cove'' dispose de cinq unités de calcul trois unités mémoire (deux spécialisées dans les écritures, trois dans les lectures). La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire.Il y a donc soit égalité, soit avantage pour les ALU entières.
Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair.
==L'unité d'émission d'un processeur superscalaire==
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Voyons pourquoi.
===L'excès de ports d'émission===
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ou 5 ?
Une solution est de réduire la largeur du processeur. Dans l'exemple précédent, il est possible de passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La solution précédente est certes très intéressante, mais elle a des défauts. Le principal est que les ports d'émission sont sous-utilisés. Et les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
===L'intérieur de l'unité d'émission===
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
==Le ''front-end'' des processeurs superscalaires==
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Le banc de registre d'un processeur superscalaire===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Une autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions auto-aligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions auto-aligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez longs, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
pf4kx7mvd2f7k2ybsbp5vr3zl64bpjz
773132
773131
2026-09-25T15:52:46Z
Mewtow
31375
/* La duplication des ALU entières */
773132
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===L'impact des contraintes d’appariement sur la performance===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour la suite du chapitre===
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==Les unités de calcul d'un processeur superscalaire==
Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. Il y a aussi plusieurs décodeur, pour décoder les instructions chargées. Après tout, si le CPU charge 4 instructions, cela demande de décoder 4 instructions, donc 4 décodeurs. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est possible d'exploiter plusieurs unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu.
Les tout premiers CPU superscalaires étaient nombreux à utiliser cette technique. Ils étaient des CPU double émission, avec une ALU entière, une unité mémoire et une FPU. Nous parlerons de '''partitionnement INT-FLOAT-MEM'''. Il s'agit de l'HyperSPARC, de l'alpha 21064 et du PowerPC 603. Les contrainte d’appariement variaient grandement suivant le processeur. Les plus permissifs autorisaient toutes les combinaisons possibles entre : une opération entière, une opération flottante, et une unité mémoire. Mais ils n'étaient pas les seuls.
Le PowerPC 603 implémentait ce genre de double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions).
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Le '''reconditionnement d'une unité fonctionnelle''' permet à celle-ci de faire des opérations qu'elle n'est pas prévue pour. Par exemple, il est possible d'utiliser l'unité mémoire comme seconde ALU entière, ou d'utiliser la FPU comme multiplieur. Ces deux exemples ne sont pas choisit au hasard, nous les étudierons dans ce qui suit. Le reconditionnement évite d'avoir à dupliquer une unité de calcul, tout en ajoutant une voie. Il permet d'améliorer l'efficacité du partitionnement, qu'il soit utilisé seul ou non.
Il est possible de reconditionner la FPU pour faire des multiplications entières. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits. Il est possible d'utiliser de multiplieur pour faire des multiplications entières 32 bits. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
Le reconditionnement de la FPU permet d'exécuter une multiplication entière en plus des autres opérations entières. Par exemple, prenons un CPU avec double émission entière-flottante, sans multiplieur entier séparé. Le reconditionnement de la FPU permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine.
Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants.
Passons maintenant à une seconde forme de reconditionnement. Reconditionner l'unité mémoire permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, des versions améliorées de l'Intel 960. L'Intel 960 originel n'était pas superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire au monde. Il intégrait une ALU entière, une unité mémoire et une unité de branchement. Grâce au partitionnement, il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, en remplacement d'une µop mémoire. Son pipeline permettait donc les combinaisons INT + BRANCH + (MEM / + INT*).
Le cas de l'i960 nous montre que le reconditionnement permet d'améliorer l'intérêt du partitionnement. Elle permet de ne pas dupliquer d'unité de calcul, tout en permettant quand même d'émettre une seconde opération entière.
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''.
Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières ! Il doit y avoir deux ALU entières pour de la double émission entière, trois pour de triple émission éntière, 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]]
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications.
Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que dupliquer l'ALU entière peut se marier parfaitement avec la présence d'une FPU. Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU reconditionne sa FPU ou pas. Mais il est très rare qu'un CPU ait à la fois un multiplieur entier et une FPU reconditionnée.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires modernes utilisent toutes ces stratégies===
Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, duplication de l'ALU entière, duplication des autre unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents.
* Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU.
* Le '''reconditionnement des unités''' a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU.
* La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants.
* La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables.
De nos jours, le partitionnement est utilisé en complément de la duplication des ALU entières, et de la duplication des autres unités. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993.
Le meilleur moyen de voir comment sont combinés ces quatre technique est de regarder ce qu'il s'est passé historiquement.
==L'évolution historique de la superscalarité==
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. La plupart des CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 sont rapidement montés à de la quadruple émission, avec quelques exceptions. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 ||
|-
! 1995
| 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000
|-
! 1996
| class="f_bleu | 620 || || || || class="f_bleu | 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
===Les processeurs historiques à double émission===
Pour commencer, étudions le cas des premiers CPU superscalaires à double émission. Ils sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Notamment, la duplication des autres unités ne sert pour ainsi dire à rien, ce qui limite les possibilités, mais rendra les explications plus simples.
Il pouvaient se classer en deux/trois types principaux, assez stricts, avec quelques processeurs qui ne rentraient pas dans ces cases. Le premier utilise le partitionnement seul, le second dupliquer les ALU entières sans partitionnement, et le troisième type est volontairement passé sous silence pour le moment (nous aborderons le troisième type dans la partie sur des design à quadruple émission).
Les CPU n'utilisant que le partitionnement faisaient la double émission avec trois unités : une ALU, une FPU et une unité mémoire. Le reconditionnement de la FPU était parfois utilisé, mais l'AGU n'était jamais reconditionnée. Si la moitié des processeurs faisait du ''partitionnement complet'' entre ALU, FPU et unité mémoire, l'autre moitié utilisait une forme encore plus simple de partitionnement.
La '''double émission entière-flottante''' consiste à émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le processeur a donc deux pipelines : un pipeline entier, et un pipeline flottant. Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire, vu que les µops mémoire passent par l'ALU entière avant d’atterrir dans l'unité mémoire. Le reconditionnement de la FPU était utilisé sur les processeurs à double émission entière-flottante. Il entrainait un léger en performance, pour un cout en circuits presque nul.
{|
|[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU.]]
|[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
|}
La seconde classe de CPU superscalaires de l'époque utilisait la '''double émission entière''', à savoir qu'ils peuvent émettre une opération en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Pour cela, ils peuvent dupliquer une ALU entière, ou reconditionner l'unité mémoire. Les exemples classiques sont les processeurs Intel : i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 reconditionnait l'unité mémoire en ALU entière.
L'Intel i960 utilisait le reconditionnement de l'AGU en plus du partitionnement, ce qui lui permettait d'exécuter une seconde instruction entière, en plus d'une autre instruction. Les instructions en question étaient simples : additions, quelques décalages. De nos jours, ce combo partitionnement et reconditionnement de l'AGU n'est plus utilisé. Il n'a pas beaucoup de sens quand on peut dupliquer des ALU entières, et les processeurs récents ont un budget en transistor suffisant pour.
Le Pentium dupliquait l'ALU entière, mais n'utilisaient aucune forme de partitionnement. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Contrairement au i960, le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
Pour résumer, voici le tableau chronologique disant quel CPU utilisait quelle technique.
* Le partitionnement INT-FLOAT-MEM est en jaune.
* La double émission entière-flottante est en vert.
* La double émission entière est en rouge.
* Les CPU barrés utilisent la quadruple émission et seront vu dans la section suivante.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| class="f_vert | RS/6000 || || || class="f_rouge | i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| class="f_vert | RSC || class="f_vert | PA 7100 || SuperSPARC || || class="f_jaune | 21064 ||
|-
! 1993
| class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || ||
|-
! 1994
| <s>POWER 2</s> || PA 7200 || || || <s>21164</s> ||
|}
Vous voyez que certains processeur sont laissés de côté dans le tableau précédent. C'est car ils ne rentrent pas dans les catégories précédentes et faisaient autrement. Si on omet le Motorola 88110, qui utilisait une forme de double émission assez avancée, les deux CPUs qui vont nous intéresser sont le PA-RISC PA 7200 d'HP, et le SuperSPARC.
Le PA 7200, sorti en 1994, est un PA-7100 auquel on aurait rajouté une ALU entière. Précisons que ce processeur utilisait aussi le reconditionnement de la FPU, comme son prédécesseur. Les combinaisons autorisées étaient donc INT + INT, INT + MEM, INT + FLOAT/MUL, MEM + FLOAT/MUL. Les combinaisons autorisées par le partitionnement INT-FLOAT-MEM étaient donc possibles, en plus des combinaisons INT + INT, voire INT + INT + MUL (vu que la FPU est reconditionnée). Autrement dit, il pouvait faire toute ce que permettait un CPU à double émission entière, plus les combinaisons FLOAT/MUL + MEM.
Le SuperSPARC était un CPU triple émission, avec deux ALU entières. Il pouvait émettre, à chaque cycle, trois µops en piochant dans deux opérations entières, un accès mémoire, une opération flottante. Le SuperSPARC avait de nombreuses contraintes d’appariement. Par exemple, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Une bonne partie de ces contraintes était lié au banc de registre, qui n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions.
: La seconde ALU a été abandonnée dans l'HyperSPARC, qui est de plus repassé en double émission, ce qui est une évolution à rebours de l'évolution des autres CPUs. Le SuperSPARC était en avance sur son temps, ce quelques années, alors que l'HyperSPARC a un design de son temps.
===Les processeurs historiques à quadruple émission===
C'est la même année , en 1994, que la quadruple émission est arrivée et que les CPU ont commencé à avoir de plus en plus d'unités de calcul. Même les designs restés en double émission avaient ajouté des unités de calcul. Il y avait alors trois stratégies pour la quadruple émission.
La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Les FPUs étaient soit dupliquées, soit scindées en deux en séparant l'additionneur du multiplieur flottant. Le POWER 2 dupliquait ses FPUs à la perfection, alors que l'21264 partitionnait la FPU. Les accès mémoire étaient traités comme des opérations entières, et le processeur pouvait : soit en faire un seul par cycle, soit en émettre deux, sous conditions (le cache devait être double port pour lire/écrire deux données à la fois, comme on le verra dans la suite du chapitre).
Les processeurs MIPS R8000 et R10000 allaient un peu plus loin, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités.
La seconde implémentation dupliquait l'ALU entière et combinait le tout avec un partitionnement INT-FLOAT-MEM. De quoi exécuter en même temps deux opérations entières, une opération flottante, et un accès mémoire. 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).
Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, avec au minimum deux ALU entières. Pour comprendre ce que je veux dire, prenons l'exemple de l'UltraSPARC. Il avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplier flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes. Le processeur pouvait émettre 4 instructions, tant qu'elles atterrissaient dans des unités différentes.
La même méthode peut être utilisée pour la double émission. Le Motorola 88110, sorti en 1991, était un CPU à double émission avec pas moins de 10 unités : deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise.
La morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes. Le tout est résumé dans le tableau ci-dessous, avec :
* en vert les CPU utilisant la première stratégie ;
* en rouge les CPU utilisant la seconde stratégie ;
* en jaune les CPU utilisant la troisième stratégie.
{|class="wikitable"
|+ Processeurs quadruple émission historiques
|-
! Année !! Processeur !! Unités
|-
! 1991
| class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques
|-
|-
! rowspan="2" | 1994
| class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'')
|-
| class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM
|-
! rowspan="3" | 1995
| class="f_jaune | PA 8000 || class="f_jaune | 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD
|-
| class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques
|-
| MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM
|-
! rowspan="2" | 1996
| class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM
|-
| class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL)
|}
===Les processeurs superscalaires modernes===
Les CPU superscalaires modernes ont des design légèrement différents des CPU historiques. Ils dupliquent l'unité entière, mais leur partitionnement est différent. En général, les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Par exemple, un CPU à 6 voies pourra émettre/exécuter 6 opérations entières simultanément, grâce à 6 ALU entières. Mais il s'agit là d'un maximum et certaines peuvent être remplacées par une opération flottante ou mémoire, qui utilisent l'unité mémoire et/ou la FPU.
Un exemple assez parlant est les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon. C'était une microarchitecture à triple émission, avec trois ALU entières et trois unités mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements.
Il s'agit là d'une tendance, cependant. Les CPU superscalaires tendent à mettre le paquet niveau opérations arithmétiques, mais pas forcément au point de remplir la totalité de la largeur du processeur. La règle est que pour N voies, le processeur dispose d'au moins N/2 ALU entières. Attention, cela ne signifie pas que le processeur émet systématiquement N/2 opération entières. Il peut par exemple émettre une combinaison d'opérations flottantes et mémoire, avec des branchements. Et N/2 est une borne basse, la plupart des CPU x86 ont environ tournent plutôt autour des 2/3, voire des 3/4.
En général, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. La conséquence est que les unités mémoire sont dupliquées, en seconde place dans l'ordre d'importance. Les processeurs x86 ont commencé à dupliquer leurs unités mémoire dès qu'ils ont atteint la quadruple émission, pour les CPU grand public. La microarchitecture Core d'Intel utilisait la quadruple émission, avec deux unités mémoire et trois ALU entières. Les microarchitectures d'AMD K5 et K6 utilisaient aussi la quadruple émission et avaient deux ALUs pour deux unités mémoire.
Plus récemment, la microarchitecture ''Golden Cove'' dispose de cinq unités de calcul trois unités mémoire (deux spécialisées dans les écritures, trois dans les lectures). La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire.Il y a donc soit égalité, soit avantage pour les ALU entières.
Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair.
==L'unité d'émission d'un processeur superscalaire==
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Voyons pourquoi.
===L'excès de ports d'émission===
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ou 5 ?
Une solution est de réduire la largeur du processeur. Dans l'exemple précédent, il est possible de passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La solution précédente est certes très intéressante, mais elle a des défauts. Le principal est que les ports d'émission sont sous-utilisés. Et les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
===L'intérieur de l'unité d'émission===
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
==Le ''front-end'' des processeurs superscalaires==
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Le banc de registre d'un processeur superscalaire===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Une autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions auto-aligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions auto-aligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez longs, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
q3e2t2r5ebv05rra7411ua9r470fvmu
773133
773132
2026-09-25T15:55:49Z
Mewtow
31375
/* Les processeurs historiques à double émission */
773133
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===L'impact des contraintes d’appariement sur la performance===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour la suite du chapitre===
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==Les unités de calcul d'un processeur superscalaire==
Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. Il y a aussi plusieurs décodeur, pour décoder les instructions chargées. Après tout, si le CPU charge 4 instructions, cela demande de décoder 4 instructions, donc 4 décodeurs. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est possible d'exploiter plusieurs unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu.
Les tout premiers CPU superscalaires étaient nombreux à utiliser cette technique. Ils étaient des CPU double émission, avec une ALU entière, une unité mémoire et une FPU. Nous parlerons de '''partitionnement INT-FLOAT-MEM'''. Il s'agit de l'HyperSPARC, de l'alpha 21064 et du PowerPC 603. Les contrainte d’appariement variaient grandement suivant le processeur. Les plus permissifs autorisaient toutes les combinaisons possibles entre : une opération entière, une opération flottante, et une unité mémoire. Mais ils n'étaient pas les seuls.
Le PowerPC 603 implémentait ce genre de double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions).
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Le '''reconditionnement d'une unité fonctionnelle''' permet à celle-ci de faire des opérations qu'elle n'est pas prévue pour. Par exemple, il est possible d'utiliser l'unité mémoire comme seconde ALU entière, ou d'utiliser la FPU comme multiplieur. Ces deux exemples ne sont pas choisit au hasard, nous les étudierons dans ce qui suit. Le reconditionnement évite d'avoir à dupliquer une unité de calcul, tout en ajoutant une voie. Il permet d'améliorer l'efficacité du partitionnement, qu'il soit utilisé seul ou non.
Il est possible de reconditionner la FPU pour faire des multiplications entières. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits. Il est possible d'utiliser de multiplieur pour faire des multiplications entières 32 bits. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
Le reconditionnement de la FPU permet d'exécuter une multiplication entière en plus des autres opérations entières. Par exemple, prenons un CPU avec double émission entière-flottante, sans multiplieur entier séparé. Le reconditionnement de la FPU permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine.
Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants.
Passons maintenant à une seconde forme de reconditionnement. Reconditionner l'unité mémoire permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, des versions améliorées de l'Intel 960. L'Intel 960 originel n'était pas superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire au monde. Il intégrait une ALU entière, une unité mémoire et une unité de branchement. Grâce au partitionnement, il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, en remplacement d'une µop mémoire. Son pipeline permettait donc les combinaisons INT + BRANCH + (MEM / + INT*).
Le cas de l'i960 nous montre que le reconditionnement permet d'améliorer l'intérêt du partitionnement. Elle permet de ne pas dupliquer d'unité de calcul, tout en permettant quand même d'émettre une seconde opération entière.
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''.
Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières ! Il doit y avoir deux ALU entières pour de la double émission entière, trois pour de triple émission éntière, 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]]
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications.
Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que dupliquer l'ALU entière peut se marier parfaitement avec la présence d'une FPU. Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU reconditionne sa FPU ou pas. Mais il est très rare qu'un CPU ait à la fois un multiplieur entier et une FPU reconditionnée.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires modernes utilisent toutes ces stratégies===
Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, duplication de l'ALU entière, duplication des autre unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents.
* Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU.
* Le '''reconditionnement des unités''' a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU.
* La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants.
* La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables.
De nos jours, le partitionnement est utilisé en complément de la duplication des ALU entières, et de la duplication des autres unités. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993.
Le meilleur moyen de voir comment sont combinés ces quatre technique est de regarder ce qu'il s'est passé historiquement.
==L'évolution historique de la superscalarité==
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. La plupart des CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 sont rapidement montés à de la quadruple émission, avec quelques exceptions. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 ||
|-
! 1995
| 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000
|-
! 1996
| class="f_bleu | 620 || || || || class="f_bleu | 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
===Les processeurs historiques à double émission===
Pour commencer, étudions le cas des premiers CPU superscalaires à double émission. Ils sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Notamment, la duplication des autres unités ne sert pour ainsi dire à rien, ce qui limite les possibilités, mais rendra les explications plus simples.
Il pouvaient se classer en deux/trois types principaux, assez stricts, avec quelques processeurs qui ne rentraient pas dans ces cases. Le premier utilise le partitionnement seul, le second dupliquer les ALU entières sans partitionnement, et le troisième type est volontairement passé sous silence pour le moment (nous aborderons le troisième type dans la partie sur des design à quadruple émission).
Les CPU n'utilisant que le partitionnement faisaient la double émission avec trois unités : une ALU, une FPU et une unité mémoire. Le reconditionnement de la FPU était parfois utilisé, mais l'AGU n'était jamais reconditionnée. Si la moitié des processeurs faisait du ''partitionnement complet'' entre ALU, FPU et unité mémoire, l'autre moitié utilisait une forme encore plus simple de partitionnement.
La '''double émission entière-flottante''' consiste à émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le processeur a donc deux pipelines : un pipeline entier, et un pipeline flottant. Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire, vu que les µops mémoire passent par l'ALU entière avant d’atterrir dans l'unité mémoire. Le reconditionnement de la FPU était utilisé sur les processeurs à double émission entière-flottante. Il entrainait un léger en performance, pour un cout en circuits presque nul.
{|
|[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU.]]
|[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
|}
La seconde classe de CPU superscalaires de l'époque utilisait la '''double émission entière''', à savoir qu'ils peuvent émettre une opération en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Pour cela, ils peuvent dupliquer une ALU entière, ou reconditionner l'unité mémoire. Les exemples classiques sont les processeurs Intel : i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 reconditionnait l'unité mémoire en ALU entière.
L'Intel i960 utilisait le reconditionnement de l'AGU en plus du partitionnement, ce qui lui permettait d'exécuter une seconde instruction entière, en plus d'une autre instruction. Les instructions en question étaient simples : additions, quelques décalages. De nos jours, ce combo partitionnement et reconditionnement de l'AGU n'est plus utilisé. Il n'a pas beaucoup de sens quand on peut dupliquer des ALU entières, et les processeurs récents ont un budget en transistor suffisant pour.
Le Pentium dupliquait l'ALU entière, mais n'utilisaient aucune forme de partitionnement. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Contrairement au i960, le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
Pour résumer, voici le tableau chronologique disant quel CPU utilisait quelle technique.
* Le partitionnement INT-FLOAT-MEM est en jaune.
* La double émission entière-flottante est en vert.
* La double émission entière est en rouge.
* Les CPU barrés utilisent la quadruple émission et seront vu dans la section suivante.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| class="f_vert | RS/6000 || || || class="f_rouge | i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| class="f_vert | RSC || class="f_vert | PA 7100 || SuperSPARC || || class="f_jaune | 21064 ||
|-
! 1993
| class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || ||
|-
! 1994
| <s>POWER 2</s> || PA 7200 || || || <s>21164</s> ||
|}
Vous voyez que certains processeur sont laissés de côté dans le tableau précédent. C'est car ils ne rentrent pas dans les catégories précédentes et faisaient autrement. Si on omet le Motorola 88110, qui utilisait une forme de double émission assez avancée, les deux CPUs qui vont nous intéresser sont le PA-RISC PA 7200 d'HP, et le SuperSPARC.
Le PA 7200, sorti en 1994, est un PA-7100 auquel on aurait rajouté une ALU entière. Précisons que ce processeur utilisait aussi le reconditionnement de la FPU, comme son prédécesseur. Les combinaisons autorisées étaient donc INT + INT, INT + MEM, INT + FLOAT/MUL, MEM + FLOAT/MUL. Les combinaisons autorisées par le partitionnement INT-FLOAT-MEM étaient donc possibles, en plus des combinaisons INT + INT, voire INT + INT + MUL (vu que la FPU est reconditionnée). Autrement dit, il pouvait faire toute ce que permettait un CPU à double émission entière, plus les combinaisons FLOAT/MUL + MEM.
Le SuperSPARC était un CPU triple émission, avec deux ALU entières, une FPU et une unité mémoire. Il pouvait émettre, à chaque cycle, trois µops en piochant dans deux opérations entières, un accès mémoire, une opération flottante. Le SuperSPARC avait de nombreuses contraintes d’appariement. Par exemple, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Une bonne partie de ces contraintes était lié au banc de registre, qui n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions.
: La seconde ALU a été abandonnée dans l'HyperSPARC, qui est de plus repassé en double émission, ce qui est une évolution à rebours de l'évolution des autres CPUs. Le SuperSPARC était en avance sur son temps, ce quelques années, alors que l'HyperSPARC a un design de son temps.
===Les processeurs historiques à quadruple émission===
C'est la même année , en 1994, que la quadruple émission est arrivée et que les CPU ont commencé à avoir de plus en plus d'unités de calcul. Même les designs restés en double émission avaient ajouté des unités de calcul. Il y avait alors trois stratégies pour la quadruple émission.
La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Les FPUs étaient soit dupliquées, soit scindées en deux en séparant l'additionneur du multiplieur flottant. Le POWER 2 dupliquait ses FPUs à la perfection, alors que l'21264 partitionnait la FPU. Les accès mémoire étaient traités comme des opérations entières, et le processeur pouvait : soit en faire un seul par cycle, soit en émettre deux, sous conditions (le cache devait être double port pour lire/écrire deux données à la fois, comme on le verra dans la suite du chapitre).
Les processeurs MIPS R8000 et R10000 allaient un peu plus loin, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités.
La seconde implémentation dupliquait l'ALU entière et combinait le tout avec un partitionnement INT-FLOAT-MEM. De quoi exécuter en même temps deux opérations entières, une opération flottante, et un accès mémoire. 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).
Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, avec au minimum deux ALU entières. Pour comprendre ce que je veux dire, prenons l'exemple de l'UltraSPARC. Il avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplier flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes. Le processeur pouvait émettre 4 instructions, tant qu'elles atterrissaient dans des unités différentes.
La même méthode peut être utilisée pour la double émission. Le Motorola 88110, sorti en 1991, était un CPU à double émission avec pas moins de 10 unités : deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise.
La morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes. Le tout est résumé dans le tableau ci-dessous, avec :
* en vert les CPU utilisant la première stratégie ;
* en rouge les CPU utilisant la seconde stratégie ;
* en jaune les CPU utilisant la troisième stratégie.
{|class="wikitable"
|+ Processeurs quadruple émission historiques
|-
! Année !! Processeur !! Unités
|-
! 1991
| class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques
|-
|-
! rowspan="2" | 1994
| class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'')
|-
| class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM
|-
! rowspan="3" | 1995
| class="f_jaune | PA 8000 || class="f_jaune | 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD
|-
| class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques
|-
| MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM
|-
! rowspan="2" | 1996
| class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM
|-
| class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL)
|}
===Les processeurs superscalaires modernes===
Les CPU superscalaires modernes ont des design légèrement différents des CPU historiques. Ils dupliquent l'unité entière, mais leur partitionnement est différent. En général, les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Par exemple, un CPU à 6 voies pourra émettre/exécuter 6 opérations entières simultanément, grâce à 6 ALU entières. Mais il s'agit là d'un maximum et certaines peuvent être remplacées par une opération flottante ou mémoire, qui utilisent l'unité mémoire et/ou la FPU.
Un exemple assez parlant est les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon. C'était une microarchitecture à triple émission, avec trois ALU entières et trois unités mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements.
Il s'agit là d'une tendance, cependant. Les CPU superscalaires tendent à mettre le paquet niveau opérations arithmétiques, mais pas forcément au point de remplir la totalité de la largeur du processeur. La règle est que pour N voies, le processeur dispose d'au moins N/2 ALU entières. Attention, cela ne signifie pas que le processeur émet systématiquement N/2 opération entières. Il peut par exemple émettre une combinaison d'opérations flottantes et mémoire, avec des branchements. Et N/2 est une borne basse, la plupart des CPU x86 ont environ tournent plutôt autour des 2/3, voire des 3/4.
En général, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. La conséquence est que les unités mémoire sont dupliquées, en seconde place dans l'ordre d'importance. Les processeurs x86 ont commencé à dupliquer leurs unités mémoire dès qu'ils ont atteint la quadruple émission, pour les CPU grand public. La microarchitecture Core d'Intel utilisait la quadruple émission, avec deux unités mémoire et trois ALU entières. Les microarchitectures d'AMD K5 et K6 utilisaient aussi la quadruple émission et avaient deux ALUs pour deux unités mémoire.
Plus récemment, la microarchitecture ''Golden Cove'' dispose de cinq unités de calcul trois unités mémoire (deux spécialisées dans les écritures, trois dans les lectures). La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire.Il y a donc soit égalité, soit avantage pour les ALU entières.
Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair.
==L'unité d'émission d'un processeur superscalaire==
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Voyons pourquoi.
===L'excès de ports d'émission===
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ou 5 ?
Une solution est de réduire la largeur du processeur. Dans l'exemple précédent, il est possible de passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La solution précédente est certes très intéressante, mais elle a des défauts. Le principal est que les ports d'émission sont sous-utilisés. Et les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
===L'intérieur de l'unité d'émission===
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
==Le ''front-end'' des processeurs superscalaires==
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Le banc de registre d'un processeur superscalaire===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Une autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions auto-aligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions auto-aligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez longs, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
a8fo6lyddp8y8sf5wwz4lcy1862y6q0
773134
773133
2026-09-25T16:50:34Z
Mewtow
31375
773134
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===L'impact des contraintes d’appariement sur la performance===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour la suite du chapitre===
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==Le ''front-end'' des processeurs superscalaires==
Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple.
Intuitivement, charger, décoder et 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 certaines processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
==Les unités de calcul d'un processeur superscalaire==
Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. Il y a aussi plusieurs décodeur, pour décoder les instructions chargées. Après tout, si le CPU charge 4 instructions, cela demande de décoder 4 instructions, donc 4 décodeurs. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est possible d'exploiter plusieurs unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu.
Les tout premiers CPU superscalaires étaient nombreux à utiliser cette technique. Ils étaient des CPU double émission, avec une ALU entière, une unité mémoire et une FPU. Nous parlerons de '''partitionnement INT-FLOAT-MEM'''. Il s'agit de l'HyperSPARC, de l'alpha 21064 et du PowerPC 603. Les contrainte d’appariement variaient grandement suivant le processeur. Les plus permissifs autorisaient toutes les combinaisons possibles entre : une opération entière, une opération flottante, et une unité mémoire. Mais ils n'étaient pas les seuls.
Le PowerPC 603 implémentait ce genre de double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions).
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Le '''reconditionnement d'une unité fonctionnelle''' permet à celle-ci de faire des opérations qu'elle n'est pas prévue pour. Par exemple, il est possible d'utiliser l'unité mémoire comme seconde ALU entière, ou d'utiliser la FPU comme multiplieur. Ces deux exemples ne sont pas choisit au hasard, nous les étudierons dans ce qui suit. Le reconditionnement évite d'avoir à dupliquer une unité de calcul, tout en ajoutant une voie. Il permet d'améliorer l'efficacité du partitionnement, qu'il soit utilisé seul ou non.
Il est possible de reconditionner la FPU pour faire des multiplications entières. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits. Il est possible d'utiliser de multiplieur pour faire des multiplications entières 32 bits. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
Le reconditionnement de la FPU permet d'exécuter une multiplication entière en plus des autres opérations entières. Par exemple, prenons un CPU avec double émission entière-flottante, sans multiplieur entier séparé. Le reconditionnement de la FPU permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine.
Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants.
Passons maintenant à une seconde forme de reconditionnement. Reconditionner l'unité mémoire permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, des versions améliorées de l'Intel 960. L'Intel 960 originel n'était pas superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire au monde. Il intégrait une ALU entière, une unité mémoire et une unité de branchement. Grâce au partitionnement, il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, en remplacement d'une µop mémoire. Son pipeline permettait donc les combinaisons INT + BRANCH + (MEM / + INT*).
Le cas de l'i960 nous montre que le reconditionnement permet d'améliorer l'intérêt du partitionnement. Elle permet de ne pas dupliquer d'unité de calcul, tout en permettant quand même d'émettre une seconde opération entière.
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''.
Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières ! Il doit y avoir deux ALU entières pour de la double émission entière, trois pour de triple émission éntière, 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]]
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications.
Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que dupliquer l'ALU entière peut se marier parfaitement avec la présence d'une FPU. Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU reconditionne sa FPU ou pas. Mais il est très rare qu'un CPU ait à la fois un multiplieur entier et une FPU reconditionnée.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires modernes utilisent toutes ces stratégies===
Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, duplication de l'ALU entière, duplication des autre unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents.
* Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU.
* Le '''reconditionnement des unités''' a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU.
* La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants.
* La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables.
De nos jours, le partitionnement est utilisé en complément de la duplication des ALU entières, et de la duplication des autres unités. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993.
Le meilleur moyen de voir comment sont combinés ces quatre technique est de regarder ce qu'il s'est passé historiquement.
==L'évolution historique de la superscalarité==
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. La plupart des CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 sont rapidement montés à de la quadruple émission, avec quelques exceptions. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 ||
|-
! 1995
| 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000
|-
! 1996
| class="f_bleu | 620 || || || || class="f_bleu | 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
===Les processeurs historiques à double émission===
Pour commencer, étudions le cas des premiers CPU superscalaires à double émission. Ils sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Notamment, la duplication des autres unités ne sert pour ainsi dire à rien, ce qui limite les possibilités, mais rendra les explications plus simples.
Il pouvaient se classer en deux/trois types principaux, assez stricts, avec quelques processeurs qui ne rentraient pas dans ces cases. Le premier utilise le partitionnement seul, le second dupliquer les ALU entières sans partitionnement, et le troisième type est volontairement passé sous silence pour le moment (nous aborderons le troisième type dans la partie sur des design à quadruple émission).
Les CPU n'utilisant que le partitionnement faisaient la double émission avec trois unités : une ALU, une FPU et une unité mémoire. Le reconditionnement de la FPU était parfois utilisé, mais l'AGU n'était jamais reconditionnée. Si la moitié des processeurs faisait du ''partitionnement complet'' entre ALU, FPU et unité mémoire, l'autre moitié utilisait une forme encore plus simple de partitionnement.
La '''double émission entière-flottante''' consiste à émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le processeur a donc deux pipelines : un pipeline entier, et un pipeline flottant. Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire, vu que les µops mémoire passent par l'ALU entière avant d’atterrir dans l'unité mémoire. Le reconditionnement de la FPU était utilisé sur les processeurs à double émission entière-flottante. Il entrainait un léger en performance, pour un cout en circuits presque nul.
{|
|[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU.]]
|[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
|}
La seconde classe de CPU superscalaires de l'époque utilisait la '''double émission entière''', à savoir qu'ils peuvent émettre une opération en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Pour cela, ils peuvent dupliquer une ALU entière, ou reconditionner l'unité mémoire. Les exemples classiques sont les processeurs Intel : i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 reconditionnait l'unité mémoire en ALU entière.
L'Intel i960 utilisait le reconditionnement de l'AGU en plus du partitionnement, ce qui lui permettait d'exécuter une seconde instruction entière, en plus d'une autre instruction. Les instructions en question étaient simples : additions, quelques décalages. De nos jours, ce combo partitionnement et reconditionnement de l'AGU n'est plus utilisé. Il n'a pas beaucoup de sens quand on peut dupliquer des ALU entières, et les processeurs récents ont un budget en transistor suffisant pour.
Le Pentium dupliquait l'ALU entière, mais n'utilisaient aucune forme de partitionnement. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Contrairement au i960, le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
Pour résumer, voici le tableau chronologique disant quel CPU utilisait quelle technique.
* Le partitionnement INT-FLOAT-MEM est en jaune.
* La double émission entière-flottante est en vert.
* La double émission entière est en rouge.
* Les CPU barrés utilisent la quadruple émission et seront vu dans la section suivante.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| class="f_vert | RS/6000 || || || class="f_rouge | i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| class="f_vert | RSC || class="f_vert | PA 7100 || SuperSPARC || || class="f_jaune | 21064 ||
|-
! 1993
| class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || ||
|-
! 1994
| <s>POWER 2</s> || PA 7200 || || || <s>21164</s> ||
|}
Vous voyez que certains processeur sont laissés de côté dans le tableau précédent. C'est car ils ne rentrent pas dans les catégories précédentes et faisaient autrement. Si on omet le Motorola 88110, qui utilisait une forme de double émission assez avancée, les deux CPUs qui vont nous intéresser sont le PA-RISC PA 7200 d'HP, et le SuperSPARC.
Le PA 7200, sorti en 1994, est un PA-7100 auquel on aurait rajouté une ALU entière. Précisons que ce processeur utilisait aussi le reconditionnement de la FPU, comme son prédécesseur. Les combinaisons autorisées étaient donc INT + INT, INT + MEM, INT + FLOAT/MUL, MEM + FLOAT/MUL. Les combinaisons autorisées par le partitionnement INT-FLOAT-MEM étaient donc possibles, en plus des combinaisons INT + INT, voire INT + INT + MUL (vu que la FPU est reconditionnée). Autrement dit, il pouvait faire toute ce que permettait un CPU à double émission entière, plus les combinaisons FLOAT/MUL + MEM.
Le SuperSPARC était un CPU triple émission, avec deux ALU entières, une FPU et une unité mémoire. Il pouvait émettre, à chaque cycle, trois µops en piochant dans deux opérations entières, un accès mémoire, une opération flottante. Le SuperSPARC avait de nombreuses contraintes d’appariement. Par exemple, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Une bonne partie de ces contraintes était lié au banc de registre, qui n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions.
: La seconde ALU a été abandonnée dans l'HyperSPARC, qui est de plus repassé en double émission, ce qui est une évolution à rebours de l'évolution des autres CPUs. Le SuperSPARC était en avance sur son temps, ce quelques années, alors que l'HyperSPARC a un design de son temps.
===Les processeurs historiques à quadruple émission===
C'est la même année , en 1994, que la quadruple émission est arrivée et que les CPU ont commencé à avoir de plus en plus d'unités de calcul. Même les designs restés en double émission avaient ajouté des unités de calcul. Il y avait alors trois stratégies pour la quadruple émission.
La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Les FPUs étaient soit dupliquées, soit scindées en deux en séparant l'additionneur du multiplieur flottant. Le POWER 2 dupliquait ses FPUs à la perfection, alors que l'21264 partitionnait la FPU. Les accès mémoire étaient traités comme des opérations entières, et le processeur pouvait : soit en faire un seul par cycle, soit en émettre deux, sous conditions (le cache devait être double port pour lire/écrire deux données à la fois, comme on le verra dans la suite du chapitre).
Les processeurs MIPS R8000 et R10000 allaient un peu plus loin, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités.
La seconde implémentation dupliquait l'ALU entière et combinait le tout avec un partitionnement INT-FLOAT-MEM. De quoi exécuter en même temps deux opérations entières, une opération flottante, et un accès mémoire. 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).
Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, avec au minimum deux ALU entières. Pour comprendre ce que je veux dire, prenons l'exemple de l'UltraSPARC. Il avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplier flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes. Le processeur pouvait émettre 4 instructions, tant qu'elles atterrissaient dans des unités différentes.
La même méthode peut être utilisée pour la double émission. Le Motorola 88110, sorti en 1991, était un CPU à double émission avec pas moins de 10 unités : deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise.
La morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes. Le tout est résumé dans le tableau ci-dessous, avec :
* en vert les CPU utilisant la première stratégie ;
* en rouge les CPU utilisant la seconde stratégie ;
* en jaune les CPU utilisant la troisième stratégie.
{|class="wikitable"
|+ Processeurs quadruple émission historiques
|-
! Année !! Processeur !! Unités
|-
! 1991
| class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques
|-
|-
! rowspan="2" | 1994
| class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'')
|-
| class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM
|-
! rowspan="3" | 1995
| class="f_jaune | PA 8000 || class="f_jaune | 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD
|-
| class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques
|-
| MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM
|-
! rowspan="2" | 1996
| class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM
|-
| class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL)
|}
===Les processeurs superscalaires modernes===
Les CPU superscalaires modernes ont des design légèrement différents des CPU historiques. Ils dupliquent l'unité entière, mais leur partitionnement est différent. En général, les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Par exemple, un CPU à 6 voies pourra émettre/exécuter 6 opérations entières simultanément, grâce à 6 ALU entières. Mais il s'agit là d'un maximum et certaines peuvent être remplacées par une opération flottante ou mémoire, qui utilisent l'unité mémoire et/ou la FPU.
Un exemple assez parlant est les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon. C'était une microarchitecture à triple émission, avec trois ALU entières et trois unités mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements.
Il s'agit là d'une tendance, cependant. Les CPU superscalaires tendent à mettre le paquet niveau opérations arithmétiques, mais pas forcément au point de remplir la totalité de la largeur du processeur. La règle est que pour N voies, le processeur dispose d'au moins N/2 ALU entières. Attention, cela ne signifie pas que le processeur émet systématiquement N/2 opération entières. Il peut par exemple émettre une combinaison d'opérations flottantes et mémoire, avec des branchements. Et N/2 est une borne basse, la plupart des CPU x86 ont environ tournent plutôt autour des 2/3, voire des 3/4.
En général, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. La conséquence est que les unités mémoire sont dupliquées, en seconde place dans l'ordre d'importance. Les processeurs x86 ont commencé à dupliquer leurs unités mémoire dès qu'ils ont atteint la quadruple émission, pour les CPU grand public. La microarchitecture Core d'Intel utilisait la quadruple émission, avec deux unités mémoire et trois ALU entières. Les microarchitectures d'AMD K5 et K6 utilisaient aussi la quadruple émission et avaient deux ALUs pour deux unités mémoire.
Plus récemment, la microarchitecture ''Golden Cove'' dispose de cinq unités de calcul trois unités mémoire (deux spécialisées dans les écritures, trois dans les lectures). La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire.Il y a donc soit égalité, soit avantage pour les ALU entières.
Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair.
==L'unité d'émission d'un processeur superscalaire==
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Voyons pourquoi.
===L'excès de ports d'émission===
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ou 5 ?
Une solution est de réduire la largeur du processeur. Dans l'exemple précédent, il est possible de passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La solution précédente est certes très intéressante, mais elle a des défauts. Le principal est que les ports d'émission sont sous-utilisés. Et les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
===L'intérieur de l'unité d'émission===
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Le banc de registre d'un processeur superscalaire===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Une autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions auto-aligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions auto-aligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez longs, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
ie84nlkst5gmfe5ysccilr9solnpkc7
773135
773134
2026-09-25T16:53:18Z
Mewtow
31375
/* Les unités de calcul d'un processeur superscalaire */
773135
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===L'impact des contraintes d’appariement sur la performance===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour la suite du chapitre===
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==Le ''front-end'' des processeurs superscalaires==
Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple.
Intuitivement, charger, décoder et 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 certaines processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
==Les unités de calcul d'un processeur superscalaire==
Après avoir vu le chargement et le décodage superscalaire, voyons ce qu'il en est au niveau des unités de calcul. Nous allons parler des unités de calcul avant de parler de l'émission, pour une raison simple : l'émission fait l'interface entre unité de calcul et ''front-end'' et on ne peut pas parler de l'émission sans avoir vu comment la superscalarité impacte les deux. Si le ''front-end'' charge et décode un paquet de N instructions, son exécution demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est possible d'exploiter plusieurs unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu.
Les tout premiers CPU superscalaires étaient nombreux à utiliser cette technique. Ils étaient des CPU double émission, avec une ALU entière, une unité mémoire et une FPU. Nous parlerons de '''partitionnement INT-FLOAT-MEM'''. Il s'agit de l'HyperSPARC, de l'alpha 21064 et du PowerPC 603. Les contrainte d’appariement variaient grandement suivant le processeur. Les plus permissifs autorisaient toutes les combinaisons possibles entre : une opération entière, une opération flottante, et une unité mémoire. Mais ils n'étaient pas les seuls.
Le PowerPC 603 implémentait ce genre de double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions).
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Le '''reconditionnement d'une unité fonctionnelle''' permet à celle-ci de faire des opérations qu'elle n'est pas prévue pour. Par exemple, il est possible d'utiliser l'unité mémoire comme seconde ALU entière, ou d'utiliser la FPU comme multiplieur. Ces deux exemples ne sont pas choisit au hasard, nous les étudierons dans ce qui suit. Le reconditionnement évite d'avoir à dupliquer une unité de calcul, tout en ajoutant une voie. Il permet d'améliorer l'efficacité du partitionnement, qu'il soit utilisé seul ou non.
Il est possible de reconditionner la FPU pour faire des multiplications entières. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits. Il est possible d'utiliser de multiplieur pour faire des multiplications entières 32 bits. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
Le reconditionnement de la FPU permet d'exécuter une multiplication entière en plus des autres opérations entières. Par exemple, prenons un CPU avec double émission entière-flottante, sans multiplieur entier séparé. Le reconditionnement de la FPU permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine.
Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants.
Passons maintenant à une seconde forme de reconditionnement. Reconditionner l'unité mémoire permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, des versions améliorées de l'Intel 960. L'Intel 960 originel n'était pas superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire au monde. Il intégrait une ALU entière, une unité mémoire et une unité de branchement. Grâce au partitionnement, il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, en remplacement d'une µop mémoire. Son pipeline permettait donc les combinaisons INT + BRANCH + (MEM / + INT*).
Le cas de l'i960 nous montre que le reconditionnement permet d'améliorer l'intérêt du partitionnement. Elle permet de ne pas dupliquer d'unité de calcul, tout en permettant quand même d'émettre une seconde opération entière.
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''.
Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières ! Il doit y avoir deux ALU entières pour de la double émission entière, trois pour de triple émission éntière, 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]]
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications.
Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que dupliquer l'ALU entière peut se marier parfaitement avec la présence d'une FPU. Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU reconditionne sa FPU ou pas. Mais il est très rare qu'un CPU ait à la fois un multiplieur entier et une FPU reconditionnée.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires modernes utilisent toutes ces stratégies===
Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, duplication de l'ALU entière, duplication des autre unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents.
* Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU.
* Le '''reconditionnement des unités''' a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU.
* La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants.
* La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables.
De nos jours, le partitionnement est utilisé en complément de la duplication des ALU entières, et de la duplication des autres unités. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993.
Le meilleur moyen de voir comment sont combinés ces quatre technique est de regarder ce qu'il s'est passé historiquement.
==L'évolution historique de la superscalarité==
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. La plupart des CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 sont rapidement montés à de la quadruple émission, avec quelques exceptions. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 ||
|-
! 1995
| 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000
|-
! 1996
| class="f_bleu | 620 || || || || class="f_bleu | 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
===Les processeurs historiques à double émission===
Pour commencer, étudions le cas des premiers CPU superscalaires à double émission. Ils sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Notamment, la duplication des autres unités ne sert pour ainsi dire à rien, ce qui limite les possibilités, mais rendra les explications plus simples.
Il pouvaient se classer en deux/trois types principaux, assez stricts, avec quelques processeurs qui ne rentraient pas dans ces cases. Le premier utilise le partitionnement seul, le second dupliquer les ALU entières sans partitionnement, et le troisième type est volontairement passé sous silence pour le moment (nous aborderons le troisième type dans la partie sur des design à quadruple émission).
Les CPU n'utilisant que le partitionnement faisaient la double émission avec trois unités : une ALU, une FPU et une unité mémoire. Le reconditionnement de la FPU était parfois utilisé, mais l'AGU n'était jamais reconditionnée. Si la moitié des processeurs faisait du ''partitionnement complet'' entre ALU, FPU et unité mémoire, l'autre moitié utilisait une forme encore plus simple de partitionnement.
La '''double émission entière-flottante''' consiste à émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le processeur a donc deux pipelines : un pipeline entier, et un pipeline flottant. Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire, vu que les µops mémoire passent par l'ALU entière avant d’atterrir dans l'unité mémoire. Le reconditionnement de la FPU était utilisé sur les processeurs à double émission entière-flottante. Il entrainait un léger en performance, pour un cout en circuits presque nul.
{|
|[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU.]]
|[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
|}
La seconde classe de CPU superscalaires de l'époque utilisait la '''double émission entière''', à savoir qu'ils peuvent émettre une opération en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Pour cela, ils peuvent dupliquer une ALU entière, ou reconditionner l'unité mémoire. Les exemples classiques sont les processeurs Intel : i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 reconditionnait l'unité mémoire en ALU entière.
L'Intel i960 utilisait le reconditionnement de l'AGU en plus du partitionnement, ce qui lui permettait d'exécuter une seconde instruction entière, en plus d'une autre instruction. Les instructions en question étaient simples : additions, quelques décalages. De nos jours, ce combo partitionnement et reconditionnement de l'AGU n'est plus utilisé. Il n'a pas beaucoup de sens quand on peut dupliquer des ALU entières, et les processeurs récents ont un budget en transistor suffisant pour.
Le Pentium dupliquait l'ALU entière, mais n'utilisaient aucune forme de partitionnement. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Contrairement au i960, le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
Pour résumer, voici le tableau chronologique disant quel CPU utilisait quelle technique.
* Le partitionnement INT-FLOAT-MEM est en jaune.
* La double émission entière-flottante est en vert.
* La double émission entière est en rouge.
* Les CPU barrés utilisent la quadruple émission et seront vu dans la section suivante.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| class="f_vert | RS/6000 || || || class="f_rouge | i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| class="f_vert | RSC || class="f_vert | PA 7100 || SuperSPARC || || class="f_jaune | 21064 ||
|-
! 1993
| class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || ||
|-
! 1994
| <s>POWER 2</s> || PA 7200 || || || <s>21164</s> ||
|}
Vous voyez que certains processeur sont laissés de côté dans le tableau précédent. C'est car ils ne rentrent pas dans les catégories précédentes et faisaient autrement. Si on omet le Motorola 88110, qui utilisait une forme de double émission assez avancée, les deux CPUs qui vont nous intéresser sont le PA-RISC PA 7200 d'HP, et le SuperSPARC.
Le PA 7200, sorti en 1994, est un PA-7100 auquel on aurait rajouté une ALU entière. Précisons que ce processeur utilisait aussi le reconditionnement de la FPU, comme son prédécesseur. Les combinaisons autorisées étaient donc INT + INT, INT + MEM, INT + FLOAT/MUL, MEM + FLOAT/MUL. Les combinaisons autorisées par le partitionnement INT-FLOAT-MEM étaient donc possibles, en plus des combinaisons INT + INT, voire INT + INT + MUL (vu que la FPU est reconditionnée). Autrement dit, il pouvait faire toute ce que permettait un CPU à double émission entière, plus les combinaisons FLOAT/MUL + MEM.
Le SuperSPARC était un CPU triple émission, avec deux ALU entières, une FPU et une unité mémoire. Il pouvait émettre, à chaque cycle, trois µops en piochant dans deux opérations entières, un accès mémoire, une opération flottante. Le SuperSPARC avait de nombreuses contraintes d’appariement. Par exemple, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Une bonne partie de ces contraintes était lié au banc de registre, qui n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions.
: La seconde ALU a été abandonnée dans l'HyperSPARC, qui est de plus repassé en double émission, ce qui est une évolution à rebours de l'évolution des autres CPUs. Le SuperSPARC était en avance sur son temps, ce quelques années, alors que l'HyperSPARC a un design de son temps.
===Les processeurs historiques à quadruple émission===
C'est la même année , en 1994, que la quadruple émission est arrivée et que les CPU ont commencé à avoir de plus en plus d'unités de calcul. Même les designs restés en double émission avaient ajouté des unités de calcul. Il y avait alors trois stratégies pour la quadruple émission.
La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Les FPUs étaient soit dupliquées, soit scindées en deux en séparant l'additionneur du multiplieur flottant. Le POWER 2 dupliquait ses FPUs à la perfection, alors que l'21264 partitionnait la FPU. Les accès mémoire étaient traités comme des opérations entières, et le processeur pouvait : soit en faire un seul par cycle, soit en émettre deux, sous conditions (le cache devait être double port pour lire/écrire deux données à la fois, comme on le verra dans la suite du chapitre).
Les processeurs MIPS R8000 et R10000 allaient un peu plus loin, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités.
La seconde implémentation dupliquait l'ALU entière et combinait le tout avec un partitionnement INT-FLOAT-MEM. De quoi exécuter en même temps deux opérations entières, une opération flottante, et un accès mémoire. 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).
Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, avec au minimum deux ALU entières. Pour comprendre ce que je veux dire, prenons l'exemple de l'UltraSPARC. Il avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplier flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes. Le processeur pouvait émettre 4 instructions, tant qu'elles atterrissaient dans des unités différentes.
La même méthode peut être utilisée pour la double émission. Le Motorola 88110, sorti en 1991, était un CPU à double émission avec pas moins de 10 unités : deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise.
La morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes. Le tout est résumé dans le tableau ci-dessous, avec :
* en vert les CPU utilisant la première stratégie ;
* en rouge les CPU utilisant la seconde stratégie ;
* en jaune les CPU utilisant la troisième stratégie.
{|class="wikitable"
|+ Processeurs quadruple émission historiques
|-
! Année !! Processeur !! Unités
|-
! 1991
| class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques
|-
|-
! rowspan="2" | 1994
| class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'')
|-
| class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM
|-
! rowspan="3" | 1995
| class="f_jaune | PA 8000 || class="f_jaune | 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD
|-
| class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques
|-
| MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM
|-
! rowspan="2" | 1996
| class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM
|-
| class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL)
|}
===Les processeurs superscalaires modernes===
Les CPU superscalaires modernes ont des design légèrement différents des CPU historiques. Ils dupliquent l'unité entière, mais leur partitionnement est différent. En général, les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Par exemple, un CPU à 6 voies pourra émettre/exécuter 6 opérations entières simultanément, grâce à 6 ALU entières. Mais il s'agit là d'un maximum et certaines peuvent être remplacées par une opération flottante ou mémoire, qui utilisent l'unité mémoire et/ou la FPU.
Un exemple assez parlant est les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon. C'était une microarchitecture à triple émission, avec trois ALU entières et trois unités mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements.
Il s'agit là d'une tendance, cependant. Les CPU superscalaires tendent à mettre le paquet niveau opérations arithmétiques, mais pas forcément au point de remplir la totalité de la largeur du processeur. La règle est que pour N voies, le processeur dispose d'au moins N/2 ALU entières. Attention, cela ne signifie pas que le processeur émet systématiquement N/2 opération entières. Il peut par exemple émettre une combinaison d'opérations flottantes et mémoire, avec des branchements. Et N/2 est une borne basse, la plupart des CPU x86 ont environ tournent plutôt autour des 2/3, voire des 3/4.
En général, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. La conséquence est que les unités mémoire sont dupliquées, en seconde place dans l'ordre d'importance. Les processeurs x86 ont commencé à dupliquer leurs unités mémoire dès qu'ils ont atteint la quadruple émission, pour les CPU grand public. La microarchitecture Core d'Intel utilisait la quadruple émission, avec deux unités mémoire et trois ALU entières. Les microarchitectures d'AMD K5 et K6 utilisaient aussi la quadruple émission et avaient deux ALUs pour deux unités mémoire.
Plus récemment, la microarchitecture ''Golden Cove'' dispose de cinq unités de calcul trois unités mémoire (deux spécialisées dans les écritures, trois dans les lectures). La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire.Il y a donc soit égalité, soit avantage pour les ALU entières.
Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair.
==L'unité d'émission d'un processeur superscalaire==
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Voyons pourquoi.
===L'excès de ports d'émission===
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ou 5 ?
Une solution est de réduire la largeur du processeur. Dans l'exemple précédent, il est possible de passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La solution précédente est certes très intéressante, mais elle a des défauts. Le principal est que les ports d'émission sont sous-utilisés. Et les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
===L'intérieur de l'unité d'émission===
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Le banc de registre d'un processeur superscalaire===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Une autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions auto-aligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions auto-aligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez longs, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
h8wbn9dvbksca1zcbdsn2tlca347qv3
773136
773135
2026-09-25T16:54:54Z
Mewtow
31375
/* Le front-end des processeurs superscalaires */
773136
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===L'impact des contraintes d’appariement sur la performance===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour la suite du chapitre===
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==Le ''front-end'' des processeurs superscalaires==
Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple.
Intuitivement, charger, décoder et 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 certaines processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. 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
|}
Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs.
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
==Les unités de calcul d'un processeur superscalaire==
Après avoir vu le chargement et le décodage superscalaire, voyons ce qu'il en est au niveau des unités de calcul. Nous allons parler des unités de calcul avant de parler de l'émission, pour une raison simple : l'émission fait l'interface entre unité de calcul et ''front-end'' et on ne peut pas parler de l'émission sans avoir vu comment la superscalarité impacte les deux. Si le ''front-end'' charge et décode un paquet de N instructions, son exécution demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est possible d'exploiter plusieurs unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu.
Les tout premiers CPU superscalaires étaient nombreux à utiliser cette technique. Ils étaient des CPU double émission, avec une ALU entière, une unité mémoire et une FPU. Nous parlerons de '''partitionnement INT-FLOAT-MEM'''. Il s'agit de l'HyperSPARC, de l'alpha 21064 et du PowerPC 603. Les contrainte d’appariement variaient grandement suivant le processeur. Les plus permissifs autorisaient toutes les combinaisons possibles entre : une opération entière, une opération flottante, et une unité mémoire. Mais ils n'étaient pas les seuls.
Le PowerPC 603 implémentait ce genre de double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions).
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Le '''reconditionnement d'une unité fonctionnelle''' permet à celle-ci de faire des opérations qu'elle n'est pas prévue pour. Par exemple, il est possible d'utiliser l'unité mémoire comme seconde ALU entière, ou d'utiliser la FPU comme multiplieur. Ces deux exemples ne sont pas choisit au hasard, nous les étudierons dans ce qui suit. Le reconditionnement évite d'avoir à dupliquer une unité de calcul, tout en ajoutant une voie. Il permet d'améliorer l'efficacité du partitionnement, qu'il soit utilisé seul ou non.
Il est possible de reconditionner la FPU pour faire des multiplications entières. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits. Il est possible d'utiliser de multiplieur pour faire des multiplications entières 32 bits. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
Le reconditionnement de la FPU permet d'exécuter une multiplication entière en plus des autres opérations entières. Par exemple, prenons un CPU avec double émission entière-flottante, sans multiplieur entier séparé. Le reconditionnement de la FPU permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine.
Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants.
Passons maintenant à une seconde forme de reconditionnement. Reconditionner l'unité mémoire permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, des versions améliorées de l'Intel 960. L'Intel 960 originel n'était pas superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire au monde. Il intégrait une ALU entière, une unité mémoire et une unité de branchement. Grâce au partitionnement, il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, en remplacement d'une µop mémoire. Son pipeline permettait donc les combinaisons INT + BRANCH + (MEM / + INT*).
Le cas de l'i960 nous montre que le reconditionnement permet d'améliorer l'intérêt du partitionnement. Elle permet de ne pas dupliquer d'unité de calcul, tout en permettant quand même d'émettre une seconde opération entière.
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''.
Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières ! Il doit y avoir deux ALU entières pour de la double émission entière, trois pour de triple émission éntière, 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]]
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications.
Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que dupliquer l'ALU entière peut se marier parfaitement avec la présence d'une FPU. Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU reconditionne sa FPU ou pas. Mais il est très rare qu'un CPU ait à la fois un multiplieur entier et une FPU reconditionnée.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires modernes utilisent toutes ces stratégies===
Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, duplication de l'ALU entière, duplication des autre unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents.
* Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU.
* Le '''reconditionnement des unités''' a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU.
* La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants.
* La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables.
De nos jours, le partitionnement est utilisé en complément de la duplication des ALU entières, et de la duplication des autres unités. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993.
Le meilleur moyen de voir comment sont combinés ces quatre technique est de regarder ce qu'il s'est passé historiquement.
==L'évolution historique de la superscalarité==
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. La plupart des CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 sont rapidement montés à de la quadruple émission, avec quelques exceptions. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 ||
|-
! 1995
| 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000
|-
! 1996
| class="f_bleu | 620 || || || || class="f_bleu | 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
===Les processeurs historiques à double émission===
Pour commencer, étudions le cas des premiers CPU superscalaires à double émission. Ils sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Notamment, la duplication des autres unités ne sert pour ainsi dire à rien, ce qui limite les possibilités, mais rendra les explications plus simples.
Il pouvaient se classer en deux/trois types principaux, assez stricts, avec quelques processeurs qui ne rentraient pas dans ces cases. Le premier utilise le partitionnement seul, le second dupliquer les ALU entières sans partitionnement, et le troisième type est volontairement passé sous silence pour le moment (nous aborderons le troisième type dans la partie sur des design à quadruple émission).
Les CPU n'utilisant que le partitionnement faisaient la double émission avec trois unités : une ALU, une FPU et une unité mémoire. Le reconditionnement de la FPU était parfois utilisé, mais l'AGU n'était jamais reconditionnée. Si la moitié des processeurs faisait du ''partitionnement complet'' entre ALU, FPU et unité mémoire, l'autre moitié utilisait une forme encore plus simple de partitionnement.
La '''double émission entière-flottante''' consiste à émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le processeur a donc deux pipelines : un pipeline entier, et un pipeline flottant. Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire, vu que les µops mémoire passent par l'ALU entière avant d’atterrir dans l'unité mémoire. Le reconditionnement de la FPU était utilisé sur les processeurs à double émission entière-flottante. Il entrainait un léger en performance, pour un cout en circuits presque nul.
{|
|[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU.]]
|[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
|}
La seconde classe de CPU superscalaires de l'époque utilisait la '''double émission entière''', à savoir qu'ils peuvent émettre une opération en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Pour cela, ils peuvent dupliquer une ALU entière, ou reconditionner l'unité mémoire. Les exemples classiques sont les processeurs Intel : i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 reconditionnait l'unité mémoire en ALU entière.
L'Intel i960 utilisait le reconditionnement de l'AGU en plus du partitionnement, ce qui lui permettait d'exécuter une seconde instruction entière, en plus d'une autre instruction. Les instructions en question étaient simples : additions, quelques décalages. De nos jours, ce combo partitionnement et reconditionnement de l'AGU n'est plus utilisé. Il n'a pas beaucoup de sens quand on peut dupliquer des ALU entières, et les processeurs récents ont un budget en transistor suffisant pour.
Le Pentium dupliquait l'ALU entière, mais n'utilisaient aucune forme de partitionnement. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Contrairement au i960, le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
Pour résumer, voici le tableau chronologique disant quel CPU utilisait quelle technique.
* Le partitionnement INT-FLOAT-MEM est en jaune.
* La double émission entière-flottante est en vert.
* La double émission entière est en rouge.
* Les CPU barrés utilisent la quadruple émission et seront vu dans la section suivante.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| class="f_vert | RS/6000 || || || class="f_rouge | i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| class="f_vert | RSC || class="f_vert | PA 7100 || SuperSPARC || || class="f_jaune | 21064 ||
|-
! 1993
| class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || ||
|-
! 1994
| <s>POWER 2</s> || PA 7200 || || || <s>21164</s> ||
|}
Vous voyez que certains processeur sont laissés de côté dans le tableau précédent. C'est car ils ne rentrent pas dans les catégories précédentes et faisaient autrement. Si on omet le Motorola 88110, qui utilisait une forme de double émission assez avancée, les deux CPUs qui vont nous intéresser sont le PA-RISC PA 7200 d'HP, et le SuperSPARC.
Le PA 7200, sorti en 1994, est un PA-7100 auquel on aurait rajouté une ALU entière. Précisons que ce processeur utilisait aussi le reconditionnement de la FPU, comme son prédécesseur. Les combinaisons autorisées étaient donc INT + INT, INT + MEM, INT + FLOAT/MUL, MEM + FLOAT/MUL. Les combinaisons autorisées par le partitionnement INT-FLOAT-MEM étaient donc possibles, en plus des combinaisons INT + INT, voire INT + INT + MUL (vu que la FPU est reconditionnée). Autrement dit, il pouvait faire toute ce que permettait un CPU à double émission entière, plus les combinaisons FLOAT/MUL + MEM.
Le SuperSPARC était un CPU triple émission, avec deux ALU entières, une FPU et une unité mémoire. Il pouvait émettre, à chaque cycle, trois µops en piochant dans deux opérations entières, un accès mémoire, une opération flottante. Le SuperSPARC avait de nombreuses contraintes d’appariement. Par exemple, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Une bonne partie de ces contraintes était lié au banc de registre, qui n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions.
: La seconde ALU a été abandonnée dans l'HyperSPARC, qui est de plus repassé en double émission, ce qui est une évolution à rebours de l'évolution des autres CPUs. Le SuperSPARC était en avance sur son temps, ce quelques années, alors que l'HyperSPARC a un design de son temps.
===Les processeurs historiques à quadruple émission===
C'est la même année , en 1994, que la quadruple émission est arrivée et que les CPU ont commencé à avoir de plus en plus d'unités de calcul. Même les designs restés en double émission avaient ajouté des unités de calcul. Il y avait alors trois stratégies pour la quadruple émission.
La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Les FPUs étaient soit dupliquées, soit scindées en deux en séparant l'additionneur du multiplieur flottant. Le POWER 2 dupliquait ses FPUs à la perfection, alors que l'21264 partitionnait la FPU. Les accès mémoire étaient traités comme des opérations entières, et le processeur pouvait : soit en faire un seul par cycle, soit en émettre deux, sous conditions (le cache devait être double port pour lire/écrire deux données à la fois, comme on le verra dans la suite du chapitre).
Les processeurs MIPS R8000 et R10000 allaient un peu plus loin, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités.
La seconde implémentation dupliquait l'ALU entière et combinait le tout avec un partitionnement INT-FLOAT-MEM. De quoi exécuter en même temps deux opérations entières, une opération flottante, et un accès mémoire. 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).
Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, avec au minimum deux ALU entières. Pour comprendre ce que je veux dire, prenons l'exemple de l'UltraSPARC. Il avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplier flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes. Le processeur pouvait émettre 4 instructions, tant qu'elles atterrissaient dans des unités différentes.
La même méthode peut être utilisée pour la double émission. Le Motorola 88110, sorti en 1991, était un CPU à double émission avec pas moins de 10 unités : deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise.
La morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes. Le tout est résumé dans le tableau ci-dessous, avec :
* en vert les CPU utilisant la première stratégie ;
* en rouge les CPU utilisant la seconde stratégie ;
* en jaune les CPU utilisant la troisième stratégie.
{|class="wikitable"
|+ Processeurs quadruple émission historiques
|-
! Année !! Processeur !! Unités
|-
! 1991
| class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques
|-
|-
! rowspan="2" | 1994
| class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'')
|-
| class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM
|-
! rowspan="3" | 1995
| class="f_jaune | PA 8000 || class="f_jaune | 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD
|-
| class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques
|-
| MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM
|-
! rowspan="2" | 1996
| class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM
|-
| class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL)
|}
===Les processeurs superscalaires modernes===
Les CPU superscalaires modernes ont des design légèrement différents des CPU historiques. Ils dupliquent l'unité entière, mais leur partitionnement est différent. En général, les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Par exemple, un CPU à 6 voies pourra émettre/exécuter 6 opérations entières simultanément, grâce à 6 ALU entières. Mais il s'agit là d'un maximum et certaines peuvent être remplacées par une opération flottante ou mémoire, qui utilisent l'unité mémoire et/ou la FPU.
Un exemple assez parlant est les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon. C'était une microarchitecture à triple émission, avec trois ALU entières et trois unités mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements.
Il s'agit là d'une tendance, cependant. Les CPU superscalaires tendent à mettre le paquet niveau opérations arithmétiques, mais pas forcément au point de remplir la totalité de la largeur du processeur. La règle est que pour N voies, le processeur dispose d'au moins N/2 ALU entières. Attention, cela ne signifie pas que le processeur émet systématiquement N/2 opération entières. Il peut par exemple émettre une combinaison d'opérations flottantes et mémoire, avec des branchements. Et N/2 est une borne basse, la plupart des CPU x86 ont environ tournent plutôt autour des 2/3, voire des 3/4.
En général, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. La conséquence est que les unités mémoire sont dupliquées, en seconde place dans l'ordre d'importance. Les processeurs x86 ont commencé à dupliquer leurs unités mémoire dès qu'ils ont atteint la quadruple émission, pour les CPU grand public. La microarchitecture Core d'Intel utilisait la quadruple émission, avec deux unités mémoire et trois ALU entières. Les microarchitectures d'AMD K5 et K6 utilisaient aussi la quadruple émission et avaient deux ALUs pour deux unités mémoire.
Plus récemment, la microarchitecture ''Golden Cove'' dispose de cinq unités de calcul trois unités mémoire (deux spécialisées dans les écritures, trois dans les lectures). La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire.Il y a donc soit égalité, soit avantage pour les ALU entières.
Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair.
==L'unité d'émission d'un processeur superscalaire==
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Voyons pourquoi.
===L'excès de ports d'émission===
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ou 5 ?
Une solution est de réduire la largeur du processeur. Dans l'exemple précédent, il est possible de passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La solution précédente est certes très intéressante, mais elle a des défauts. Le principal est que les ports d'émission sont sous-utilisés. Et les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
===L'intérieur de l'unité d'émission===
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Le banc de registre d'un processeur superscalaire===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Une autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions auto-aligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions auto-aligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez longs, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
ega6298u9ynx2k4xt83u8uvbf6g8kt9
773137
773136
2026-09-25T17:03:12Z
Mewtow
31375
/* Le partitionnement intra-unité */
773137
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===L'impact des contraintes d’appariement sur la performance===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour la suite du chapitre===
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==Le ''front-end'' des processeurs superscalaires==
Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple.
Intuitivement, charger, décoder et 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 certaines processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. 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
|}
Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs.
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
==Les unités de calcul d'un processeur superscalaire==
Après avoir vu le chargement et le décodage superscalaire, voyons ce qu'il en est au niveau des unités de calcul. Nous allons parler des unités de calcul avant de parler de l'émission, pour une raison simple : l'émission fait l'interface entre unité de calcul et ''front-end'' et on ne peut pas parler de l'émission sans avoir vu comment la superscalarité impacte les deux. Si le ''front-end'' charge et décode un paquet de N instructions, son exécution demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est possible d'exploiter plusieurs unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu.
Les tout premiers CPU superscalaires étaient nombreux à utiliser cette technique. Ils étaient des CPU double émission, avec une ALU entière, une unité mémoire et une FPU. Nous parlerons de '''partitionnement INT-FLOAT-MEM'''. Il s'agit de l'HyperSPARC, de l'alpha 21064 et du PowerPC 603. Les contrainte d’appariement variaient grandement suivant le processeur. Les plus permissifs autorisaient toutes les combinaisons possibles entre : une opération entière, une opération flottante, et une unité mémoire. Mais ils n'étaient pas les seuls.
Le PowerPC 603 implémentait ce genre de double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions).
Il est aussi possible de faire la même chose pour les opérations entières, vu qu'elles sont prises en charge par trois circuits. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres. Il faut parfois lui ajouter le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible d'émettre une opération séparément, ce qui donne le '''partitionnement entier''' dans ce qui suit. Le partitionnement entier n'est jamais utilisé tel quel, car le gain en performance serait trop faible au regard des couts. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Le '''reconditionnement d'une unité fonctionnelle''' permet à celle-ci de faire des opérations qu'elle n'est pas prévue pour. Par exemple, il est possible d'utiliser l'unité mémoire comme seconde ALU entière, ou d'utiliser la FPU comme multiplieur. Ces deux exemples ne sont pas choisit au hasard, nous les étudierons dans ce qui suit. Le reconditionnement évite d'avoir à dupliquer une unité de calcul, tout en ajoutant une voie. Il permet d'améliorer l'efficacité du partitionnement, qu'il soit utilisé seul ou non.
Il est possible de reconditionner la FPU pour faire des multiplications entières. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits. Il est possible d'utiliser de multiplieur pour faire des multiplications entières 32 bits. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
Le reconditionnement de la FPU permet d'exécuter une multiplication entière en plus des autres opérations entières. Par exemple, prenons un CPU avec double émission entière-flottante, sans multiplieur entier séparé. Le reconditionnement de la FPU permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine.
Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants.
Passons maintenant à une seconde forme de reconditionnement. Reconditionner l'unité mémoire permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, des versions améliorées de l'Intel 960. L'Intel 960 originel n'était pas superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire au monde. Il intégrait une ALU entière, une unité mémoire et une unité de branchement. Grâce au partitionnement, il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, en remplacement d'une µop mémoire. Son pipeline permettait donc les combinaisons INT + BRANCH + (MEM / + INT*).
Le cas de l'i960 nous montre que le reconditionnement permet d'améliorer l'intérêt du partitionnement. Elle permet de ne pas dupliquer d'unité de calcul, tout en permettant quand même d'émettre une seconde opération entière.
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''.
Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières ! Il doit y avoir deux ALU entières pour de la double émission entière, trois pour de triple émission éntière, 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]]
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications.
Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que dupliquer l'ALU entière peut se marier parfaitement avec la présence d'une FPU. Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU reconditionne sa FPU ou pas. Mais il est très rare qu'un CPU ait à la fois un multiplieur entier et une FPU reconditionnée.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires modernes utilisent toutes ces stratégies===
Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, duplication de l'ALU entière, duplication des autre unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents.
* Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU.
* Le '''reconditionnement des unités''' a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU.
* La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants.
* La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables.
De nos jours, le partitionnement est utilisé en complément de la duplication des ALU entières, et de la duplication des autres unités. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993.
Le meilleur moyen de voir comment sont combinés ces quatre technique est de regarder ce qu'il s'est passé historiquement.
==L'évolution historique de la superscalarité==
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. La plupart des CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 sont rapidement montés à de la quadruple émission, avec quelques exceptions. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 ||
|-
! 1995
| 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000
|-
! 1996
| class="f_bleu | 620 || || || || class="f_bleu | 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
===Les processeurs historiques à double émission===
Pour commencer, étudions le cas des premiers CPU superscalaires à double émission. Ils sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Notamment, la duplication des autres unités ne sert pour ainsi dire à rien, ce qui limite les possibilités, mais rendra les explications plus simples.
Il pouvaient se classer en deux/trois types principaux, assez stricts, avec quelques processeurs qui ne rentraient pas dans ces cases. Le premier utilise le partitionnement seul, le second dupliquer les ALU entières sans partitionnement, et le troisième type est volontairement passé sous silence pour le moment (nous aborderons le troisième type dans la partie sur des design à quadruple émission).
Les CPU n'utilisant que le partitionnement faisaient la double émission avec trois unités : une ALU, une FPU et une unité mémoire. Le reconditionnement de la FPU était parfois utilisé, mais l'AGU n'était jamais reconditionnée. Si la moitié des processeurs faisait du ''partitionnement complet'' entre ALU, FPU et unité mémoire, l'autre moitié utilisait une forme encore plus simple de partitionnement.
La '''double émission entière-flottante''' consiste à émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le processeur a donc deux pipelines : un pipeline entier, et un pipeline flottant. Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire, vu que les µops mémoire passent par l'ALU entière avant d’atterrir dans l'unité mémoire. Le reconditionnement de la FPU était utilisé sur les processeurs à double émission entière-flottante. Il entrainait un léger en performance, pour un cout en circuits presque nul.
{|
|[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU.]]
|[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
|}
La seconde classe de CPU superscalaires de l'époque utilisait la '''double émission entière''', à savoir qu'ils peuvent émettre une opération en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Pour cela, ils peuvent dupliquer une ALU entière, ou reconditionner l'unité mémoire. Les exemples classiques sont les processeurs Intel : i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 reconditionnait l'unité mémoire en ALU entière.
L'Intel i960 utilisait le reconditionnement de l'AGU en plus du partitionnement, ce qui lui permettait d'exécuter une seconde instruction entière, en plus d'une autre instruction. Les instructions en question étaient simples : additions, quelques décalages. De nos jours, ce combo partitionnement et reconditionnement de l'AGU n'est plus utilisé. Il n'a pas beaucoup de sens quand on peut dupliquer des ALU entières, et les processeurs récents ont un budget en transistor suffisant pour.
Le Pentium dupliquait l'ALU entière, mais n'utilisaient aucune forme de partitionnement. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Contrairement au i960, le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
Pour résumer, voici le tableau chronologique disant quel CPU utilisait quelle technique.
* Le partitionnement INT-FLOAT-MEM est en jaune.
* La double émission entière-flottante est en vert.
* La double émission entière est en rouge.
* Les CPU barrés utilisent la quadruple émission et seront vu dans la section suivante.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| class="f_vert | RS/6000 || || || class="f_rouge | i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| class="f_vert | RSC || class="f_vert | PA 7100 || SuperSPARC || || class="f_jaune | 21064 ||
|-
! 1993
| class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || ||
|-
! 1994
| <s>POWER 2</s> || PA 7200 || || || <s>21164</s> ||
|}
Vous voyez que certains processeur sont laissés de côté dans le tableau précédent. C'est car ils ne rentrent pas dans les catégories précédentes et faisaient autrement. Si on omet le Motorola 88110, qui utilisait une forme de double émission assez avancée, les deux CPUs qui vont nous intéresser sont le PA-RISC PA 7200 d'HP, et le SuperSPARC.
Le PA 7200, sorti en 1994, est un PA-7100 auquel on aurait rajouté une ALU entière. Précisons que ce processeur utilisait aussi le reconditionnement de la FPU, comme son prédécesseur. Les combinaisons autorisées étaient donc INT + INT, INT + MEM, INT + FLOAT/MUL, MEM + FLOAT/MUL. Les combinaisons autorisées par le partitionnement INT-FLOAT-MEM étaient donc possibles, en plus des combinaisons INT + INT, voire INT + INT + MUL (vu que la FPU est reconditionnée). Autrement dit, il pouvait faire toute ce que permettait un CPU à double émission entière, plus les combinaisons FLOAT/MUL + MEM.
Le SuperSPARC était un CPU triple émission, avec deux ALU entières, une FPU et une unité mémoire. Il pouvait émettre, à chaque cycle, trois µops en piochant dans deux opérations entières, un accès mémoire, une opération flottante. Le SuperSPARC avait de nombreuses contraintes d’appariement. Par exemple, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Une bonne partie de ces contraintes était lié au banc de registre, qui n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions.
: La seconde ALU a été abandonnée dans l'HyperSPARC, qui est de plus repassé en double émission, ce qui est une évolution à rebours de l'évolution des autres CPUs. Le SuperSPARC était en avance sur son temps, ce quelques années, alors que l'HyperSPARC a un design de son temps.
===Les processeurs historiques à quadruple émission===
C'est la même année , en 1994, que la quadruple émission est arrivée et que les CPU ont commencé à avoir de plus en plus d'unités de calcul. Même les designs restés en double émission avaient ajouté des unités de calcul. Il y avait alors trois stratégies pour la quadruple émission.
La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Les FPUs étaient soit dupliquées, soit scindées en deux en séparant l'additionneur du multiplieur flottant. Le POWER 2 dupliquait ses FPUs à la perfection, alors que l'21264 partitionnait la FPU. Les accès mémoire étaient traités comme des opérations entières, et le processeur pouvait : soit en faire un seul par cycle, soit en émettre deux, sous conditions (le cache devait être double port pour lire/écrire deux données à la fois, comme on le verra dans la suite du chapitre).
Les processeurs MIPS R8000 et R10000 allaient un peu plus loin, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités.
La seconde implémentation dupliquait l'ALU entière et combinait le tout avec un partitionnement INT-FLOAT-MEM. De quoi exécuter en même temps deux opérations entières, une opération flottante, et un accès mémoire. 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).
Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, avec au minimum deux ALU entières. Pour comprendre ce que je veux dire, prenons l'exemple de l'UltraSPARC. Il avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplier flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes. Le processeur pouvait émettre 4 instructions, tant qu'elles atterrissaient dans des unités différentes.
La même méthode peut être utilisée pour la double émission. Le Motorola 88110, sorti en 1991, était un CPU à double émission avec pas moins de 10 unités : deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise.
La morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes. Le tout est résumé dans le tableau ci-dessous, avec :
* en vert les CPU utilisant la première stratégie ;
* en rouge les CPU utilisant la seconde stratégie ;
* en jaune les CPU utilisant la troisième stratégie.
{|class="wikitable"
|+ Processeurs quadruple émission historiques
|-
! Année !! Processeur !! Unités
|-
! 1991
| class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques
|-
|-
! rowspan="2" | 1994
| class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'')
|-
| class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM
|-
! rowspan="3" | 1995
| class="f_jaune | PA 8000 || class="f_jaune | 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD
|-
| class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques
|-
| MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM
|-
! rowspan="2" | 1996
| class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM
|-
| class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL)
|}
===Les processeurs superscalaires modernes===
Les CPU superscalaires modernes ont des design légèrement différents des CPU historiques. Ils dupliquent l'unité entière, mais leur partitionnement est différent. En général, les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Par exemple, un CPU à 6 voies pourra émettre/exécuter 6 opérations entières simultanément, grâce à 6 ALU entières. Mais il s'agit là d'un maximum et certaines peuvent être remplacées par une opération flottante ou mémoire, qui utilisent l'unité mémoire et/ou la FPU.
Un exemple assez parlant est les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon. C'était une microarchitecture à triple émission, avec trois ALU entières et trois unités mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements.
Il s'agit là d'une tendance, cependant. Les CPU superscalaires tendent à mettre le paquet niveau opérations arithmétiques, mais pas forcément au point de remplir la totalité de la largeur du processeur. La règle est que pour N voies, le processeur dispose d'au moins N/2 ALU entières. Attention, cela ne signifie pas que le processeur émet systématiquement N/2 opération entières. Il peut par exemple émettre une combinaison d'opérations flottantes et mémoire, avec des branchements. Et N/2 est une borne basse, la plupart des CPU x86 ont environ tournent plutôt autour des 2/3, voire des 3/4.
En général, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. La conséquence est que les unités mémoire sont dupliquées, en seconde place dans l'ordre d'importance. Les processeurs x86 ont commencé à dupliquer leurs unités mémoire dès qu'ils ont atteint la quadruple émission, pour les CPU grand public. La microarchitecture Core d'Intel utilisait la quadruple émission, avec deux unités mémoire et trois ALU entières. Les microarchitectures d'AMD K5 et K6 utilisaient aussi la quadruple émission et avaient deux ALUs pour deux unités mémoire.
Plus récemment, la microarchitecture ''Golden Cove'' dispose de cinq unités de calcul trois unités mémoire (deux spécialisées dans les écritures, trois dans les lectures). La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire.Il y a donc soit égalité, soit avantage pour les ALU entières.
Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair.
==L'unité d'émission d'un processeur superscalaire==
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Voyons pourquoi.
===L'excès de ports d'émission===
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ou 5 ?
Une solution est de réduire la largeur du processeur. Dans l'exemple précédent, il est possible de passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La solution précédente est certes très intéressante, mais elle a des défauts. Le principal est que les ports d'émission sont sous-utilisés. Et les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
===L'intérieur de l'unité d'émission===
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Le banc de registre d'un processeur superscalaire===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Une autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions auto-aligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions auto-aligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez longs, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
c377m0gj4sfm6nvrpiye0v54o6an5f3
773138
773137
2026-09-25T17:04:09Z
Mewtow
31375
/* Les processeurs historiques à double émission */
773138
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===L'impact des contraintes d’appariement sur la performance===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour la suite du chapitre===
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==Le ''front-end'' des processeurs superscalaires==
Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple.
Intuitivement, charger, décoder et 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 certaines processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. 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
|}
Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs.
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
==Les unités de calcul d'un processeur superscalaire==
Après avoir vu le chargement et le décodage superscalaire, voyons ce qu'il en est au niveau des unités de calcul. Nous allons parler des unités de calcul avant de parler de l'émission, pour une raison simple : l'émission fait l'interface entre unité de calcul et ''front-end'' et on ne peut pas parler de l'émission sans avoir vu comment la superscalarité impacte les deux. Si le ''front-end'' charge et décode un paquet de N instructions, son exécution demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est possible d'exploiter plusieurs unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu.
Les tout premiers CPU superscalaires étaient nombreux à utiliser cette technique. Ils étaient des CPU double émission, avec une ALU entière, une unité mémoire et une FPU. Nous parlerons de '''partitionnement INT-FLOAT-MEM'''. Il s'agit de l'HyperSPARC, de l'alpha 21064 et du PowerPC 603. Les contrainte d’appariement variaient grandement suivant le processeur. Les plus permissifs autorisaient toutes les combinaisons possibles entre : une opération entière, une opération flottante, et une unité mémoire. Mais ils n'étaient pas les seuls.
Le PowerPC 603 implémentait ce genre de double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions).
Il est aussi possible de faire la même chose pour les opérations entières, vu qu'elles sont prises en charge par trois circuits. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres. Il faut parfois lui ajouter le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible d'émettre une opération séparément, ce qui donne le '''partitionnement entier''' dans ce qui suit. Le partitionnement entier n'est jamais utilisé tel quel, car le gain en performance serait trop faible au regard des couts. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Le '''reconditionnement d'une unité fonctionnelle''' permet à celle-ci de faire des opérations qu'elle n'est pas prévue pour. Par exemple, il est possible d'utiliser l'unité mémoire comme seconde ALU entière, ou d'utiliser la FPU comme multiplieur. Ces deux exemples ne sont pas choisit au hasard, nous les étudierons dans ce qui suit. Le reconditionnement évite d'avoir à dupliquer une unité de calcul, tout en ajoutant une voie. Il permet d'améliorer l'efficacité du partitionnement, qu'il soit utilisé seul ou non.
Il est possible de reconditionner la FPU pour faire des multiplications entières. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits. Il est possible d'utiliser de multiplieur pour faire des multiplications entières 32 bits. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
Le reconditionnement de la FPU permet d'exécuter une multiplication entière en plus des autres opérations entières. Par exemple, prenons un CPU avec double émission entière-flottante, sans multiplieur entier séparé. Le reconditionnement de la FPU permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine.
Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants.
Passons maintenant à une seconde forme de reconditionnement. Reconditionner l'unité mémoire permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, des versions améliorées de l'Intel 960. L'Intel 960 originel n'était pas superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire au monde. Il intégrait une ALU entière, une unité mémoire et une unité de branchement. Grâce au partitionnement, il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, en remplacement d'une µop mémoire. Son pipeline permettait donc les combinaisons INT + BRANCH + (MEM / + INT*).
Le cas de l'i960 nous montre que le reconditionnement permet d'améliorer l'intérêt du partitionnement. Elle permet de ne pas dupliquer d'unité de calcul, tout en permettant quand même d'émettre une seconde opération entière.
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''.
Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières ! Il doit y avoir deux ALU entières pour de la double émission entière, trois pour de triple émission éntière, 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]]
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications.
Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que dupliquer l'ALU entière peut se marier parfaitement avec la présence d'une FPU. Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU reconditionne sa FPU ou pas. Mais il est très rare qu'un CPU ait à la fois un multiplieur entier et une FPU reconditionnée.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires modernes utilisent toutes ces stratégies===
Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, duplication de l'ALU entière, duplication des autre unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents.
* Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU.
* Le '''reconditionnement des unités''' a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU.
* La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants.
* La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables.
De nos jours, le partitionnement est utilisé en complément de la duplication des ALU entières, et de la duplication des autres unités. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993.
Le meilleur moyen de voir comment sont combinés ces quatre technique est de regarder ce qu'il s'est passé historiquement.
==L'évolution historique de la superscalarité==
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. La plupart des CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 sont rapidement montés à de la quadruple émission, avec quelques exceptions. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 ||
|-
! 1995
| 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000
|-
! 1996
| class="f_bleu | 620 || || || || class="f_bleu | 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
===Les processeurs historiques à double émission===
Pour commencer, étudions le cas des premiers CPU superscalaires à double émission. Ils sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Notamment, la duplication des autres unités ne sert pour ainsi dire à rien, ce qui limite les possibilités, mais rendra les explications plus simples.
Il pouvaient se classer en deux/trois types principaux, assez stricts, avec quelques processeurs qui ne rentraient pas dans ces cases. Le premier utilise le partitionnement seul, le second dupliquer les ALU entières sans partitionnement, et le troisième type est volontairement passé sous silence pour le moment (nous aborderons le troisième type dans la partie sur des design à quadruple émission).
Les CPU n'utilisant que le partitionnement faisaient la double émission avec trois unités : une ALU, une FPU et une unité mémoire. Le reconditionnement de la FPU était parfois utilisé, mais l'AGU n'était jamais reconditionnée. Si la moitié des processeurs faisait du ''partitionnement complet'' entre ALU, FPU et unité mémoire, l'autre moitié utilisait une forme encore plus simple de partitionnement.
La '''double émission entière-flottante''' consiste à émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le processeur a donc deux pipelines : un pipeline entier, et un pipeline flottant. Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire, vu que les µops mémoire passent par l'ALU entière avant d’atterrir dans l'unité mémoire. Le reconditionnement de la FPU était utilisé sur les processeurs à double émission entière-flottante. Il entrainait un léger en performance, pour un cout en circuits presque nul.
{|
|[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU.]]
|[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
|}
La seconde classe de CPU superscalaires de l'époque utilisait la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Pour cela, ils peuvent dupliquer une ALU entière, ou reconditionner l'unité mémoire. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 reconditionnait l'unité mémoire en ALU entière.
L'Intel i960 utilisait le reconditionnement de l'AGU en plus du partitionnement, ce qui lui permettait d'exécuter une seconde instruction entière, en plus d'une autre instruction. Les instructions en question étaient simples : additions, quelques décalages. De nos jours, ce combo partitionnement et reconditionnement de l'AGU n'est plus utilisé. Il n'a pas beaucoup de sens quand on peut dupliquer des ALU entières, et les processeurs récents ont un budget en transistor suffisant pour.
Le Pentium dupliquait l'ALU entière, mais n'utilisaient aucune forme de partitionnement. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Contrairement au i960, le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
Pour résumer, voici le tableau chronologique disant quel CPU utilisait quelle technique.
* Le partitionnement INT-FLOAT-MEM est en jaune.
* La double émission entière-flottante est en vert.
* La double émission entière est en rouge.
* Les CPU barrés utilisent la quadruple émission et seront vu dans la section suivante.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| class="f_vert | RS/6000 || || || class="f_rouge | i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| class="f_vert | RSC || class="f_vert | PA 7100 || SuperSPARC || || class="f_jaune | 21064 ||
|-
! 1993
| class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || ||
|-
! 1994
| <s>POWER 2</s> || PA 7200 || || || <s>21164</s> ||
|}
Vous voyez que certains processeur sont laissés de côté dans le tableau précédent. C'est car ils ne rentrent pas dans les catégories précédentes et faisaient autrement. Si on omet le Motorola 88110, qui utilisait une forme de double émission assez avancée, les deux CPUs qui vont nous intéresser sont le PA-RISC PA 7200 d'HP, et le SuperSPARC.
Le PA 7200, sorti en 1994, est un PA-7100 auquel on aurait rajouté une ALU entière. Précisons que ce processeur utilisait aussi le reconditionnement de la FPU, comme son prédécesseur. Les combinaisons autorisées étaient donc INT + INT, INT + MEM, INT + FLOAT/MUL, MEM + FLOAT/MUL. Les combinaisons autorisées par le partitionnement INT-FLOAT-MEM étaient donc possibles, en plus des combinaisons INT + INT, voire INT + INT + MUL (vu que la FPU est reconditionnée). Autrement dit, il pouvait faire toute ce que permettait un CPU à double émission entière, plus les combinaisons FLOAT/MUL + MEM.
Le SuperSPARC était un CPU triple émission, avec deux ALU entières, une FPU et une unité mémoire. Il pouvait émettre, à chaque cycle, trois µops en piochant dans deux opérations entières, un accès mémoire, une opération flottante. Le SuperSPARC avait de nombreuses contraintes d’appariement. Par exemple, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Une bonne partie de ces contraintes était lié au banc de registre, qui n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions.
: La seconde ALU a été abandonnée dans l'HyperSPARC, qui est de plus repassé en double émission, ce qui est une évolution à rebours de l'évolution des autres CPUs. Le SuperSPARC était en avance sur son temps, ce quelques années, alors que l'HyperSPARC a un design de son temps.
===Les processeurs historiques à quadruple émission===
C'est la même année , en 1994, que la quadruple émission est arrivée et que les CPU ont commencé à avoir de plus en plus d'unités de calcul. Même les designs restés en double émission avaient ajouté des unités de calcul. Il y avait alors trois stratégies pour la quadruple émission.
La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Les FPUs étaient soit dupliquées, soit scindées en deux en séparant l'additionneur du multiplieur flottant. Le POWER 2 dupliquait ses FPUs à la perfection, alors que l'21264 partitionnait la FPU. Les accès mémoire étaient traités comme des opérations entières, et le processeur pouvait : soit en faire un seul par cycle, soit en émettre deux, sous conditions (le cache devait être double port pour lire/écrire deux données à la fois, comme on le verra dans la suite du chapitre).
Les processeurs MIPS R8000 et R10000 allaient un peu plus loin, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités.
La seconde implémentation dupliquait l'ALU entière et combinait le tout avec un partitionnement INT-FLOAT-MEM. De quoi exécuter en même temps deux opérations entières, une opération flottante, et un accès mémoire. 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).
Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, avec au minimum deux ALU entières. Pour comprendre ce que je veux dire, prenons l'exemple de l'UltraSPARC. Il avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplier flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes. Le processeur pouvait émettre 4 instructions, tant qu'elles atterrissaient dans des unités différentes.
La même méthode peut être utilisée pour la double émission. Le Motorola 88110, sorti en 1991, était un CPU à double émission avec pas moins de 10 unités : deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise.
La morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes. Le tout est résumé dans le tableau ci-dessous, avec :
* en vert les CPU utilisant la première stratégie ;
* en rouge les CPU utilisant la seconde stratégie ;
* en jaune les CPU utilisant la troisième stratégie.
{|class="wikitable"
|+ Processeurs quadruple émission historiques
|-
! Année !! Processeur !! Unités
|-
! 1991
| class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques
|-
|-
! rowspan="2" | 1994
| class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'')
|-
| class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM
|-
! rowspan="3" | 1995
| class="f_jaune | PA 8000 || class="f_jaune | 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD
|-
| class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques
|-
| MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM
|-
! rowspan="2" | 1996
| class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM
|-
| class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL)
|}
===Les processeurs superscalaires modernes===
Les CPU superscalaires modernes ont des design légèrement différents des CPU historiques. Ils dupliquent l'unité entière, mais leur partitionnement est différent. En général, les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Par exemple, un CPU à 6 voies pourra émettre/exécuter 6 opérations entières simultanément, grâce à 6 ALU entières. Mais il s'agit là d'un maximum et certaines peuvent être remplacées par une opération flottante ou mémoire, qui utilisent l'unité mémoire et/ou la FPU.
Un exemple assez parlant est les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon. C'était une microarchitecture à triple émission, avec trois ALU entières et trois unités mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements.
Il s'agit là d'une tendance, cependant. Les CPU superscalaires tendent à mettre le paquet niveau opérations arithmétiques, mais pas forcément au point de remplir la totalité de la largeur du processeur. La règle est que pour N voies, le processeur dispose d'au moins N/2 ALU entières. Attention, cela ne signifie pas que le processeur émet systématiquement N/2 opération entières. Il peut par exemple émettre une combinaison d'opérations flottantes et mémoire, avec des branchements. Et N/2 est une borne basse, la plupart des CPU x86 ont environ tournent plutôt autour des 2/3, voire des 3/4.
En général, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. La conséquence est que les unités mémoire sont dupliquées, en seconde place dans l'ordre d'importance. Les processeurs x86 ont commencé à dupliquer leurs unités mémoire dès qu'ils ont atteint la quadruple émission, pour les CPU grand public. La microarchitecture Core d'Intel utilisait la quadruple émission, avec deux unités mémoire et trois ALU entières. Les microarchitectures d'AMD K5 et K6 utilisaient aussi la quadruple émission et avaient deux ALUs pour deux unités mémoire.
Plus récemment, la microarchitecture ''Golden Cove'' dispose de cinq unités de calcul trois unités mémoire (deux spécialisées dans les écritures, trois dans les lectures). La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire.Il y a donc soit égalité, soit avantage pour les ALU entières.
Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair.
==L'unité d'émission d'un processeur superscalaire==
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Voyons pourquoi.
===L'excès de ports d'émission===
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ou 5 ?
Une solution est de réduire la largeur du processeur. Dans l'exemple précédent, il est possible de passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La solution précédente est certes très intéressante, mais elle a des défauts. Le principal est que les ports d'émission sont sous-utilisés. Et les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
===L'intérieur de l'unité d'émission===
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Le banc de registre d'un processeur superscalaire===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Une autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions auto-aligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions auto-aligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez longs, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
3yun2tf24gg4w8drhw6u8n4o65vcuc3
773139
773138
2026-09-25T18:44:18Z
Mewtow
31375
773139
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===L'impact des contraintes d’appariement sur la performance===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour la suite du chapitre===
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==Le ''front-end'' des processeurs superscalaires==
Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple.
Intuitivement, charger, décoder et 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 certaines processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. 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
|}
Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs.
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
==Les unités de calcul d'un processeur superscalaire==
Après avoir vu le chargement et le décodage superscalaire, voyons ce qu'il en est au niveau des unités de calcul. Nous allons parler des unités de calcul avant de parler de l'émission, pour une raison simple : l'émission fait l'interface entre unité de calcul et ''front-end'' et on ne peut pas parler de l'émission sans avoir vu comment la superscalarité impacte les deux. Si le ''front-end'' charge et décode un paquet de N instructions, son exécution demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est possible d'exploiter plusieurs unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu.
Les tout premiers CPU superscalaires étaient nombreux à utiliser cette technique. Ils étaient des CPU double émission, avec une ALU entière, une unité mémoire et une FPU. Nous parlerons de '''partitionnement INT-FLOAT-MEM'''. Il s'agit de l'HyperSPARC, de l'alpha 21064 et du PowerPC 603. Les contrainte d’appariement variaient grandement suivant le processeur. Les plus permissifs autorisaient toutes les combinaisons possibles entre : une opération entière, une opération flottante, et une unité mémoire. Mais ils n'étaient pas les seuls.
Le PowerPC 603 implémentait ce genre de double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions).
Il est aussi possible de faire la même chose pour les opérations entières, vu qu'elles sont prises en charge par trois circuits. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres. Il faut parfois lui ajouter le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible d'émettre une opération séparément, ce qui donne le '''partitionnement entier''' dans ce qui suit. Le partitionnement entier n'est jamais utilisé tel quel, car le gain en performance serait trop faible au regard des couts. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Le '''reconditionnement d'une unité fonctionnelle''' permet à celle-ci de faire des opérations qu'elle n'est pas prévue pour. Par exemple, il est possible d'utiliser l'unité mémoire comme seconde ALU entière, ou d'utiliser la FPU comme multiplieur. Ces deux exemples ne sont pas choisit au hasard, nous les étudierons dans ce qui suit. Le reconditionnement évite d'avoir à dupliquer une unité de calcul, tout en ajoutant une voie. Il permet d'améliorer l'efficacité du partitionnement, qu'il soit utilisé seul ou non.
Il est possible de reconditionner la FPU pour faire des multiplications entières. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits. Il est possible d'utiliser de multiplieur pour faire des multiplications entières 32 bits. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
Le reconditionnement de la FPU permet d'exécuter une multiplication entière en plus des autres opérations entières. Par exemple, prenons un CPU avec double émission entière-flottante, sans multiplieur entier séparé. Le reconditionnement de la FPU permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine.
Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants.
Passons maintenant à une seconde forme de reconditionnement. Reconditionner l'unité mémoire permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, des versions améliorées de l'Intel 960. L'Intel 960 originel n'était pas superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire au monde. Il intégrait une ALU entière, une unité mémoire et une unité de branchement. Grâce au partitionnement, il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, en remplacement d'une µop mémoire. Son pipeline permettait donc les combinaisons INT + BRANCH + (MEM / + INT*).
Le cas de l'i960 nous montre que le reconditionnement permet d'améliorer l'intérêt du partitionnement. Elle permet de ne pas dupliquer d'unité de calcul, tout en permettant quand même d'émettre une seconde opération entière.
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''.
Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières ! Il doit y avoir deux ALU entières pour de la double émission entière, trois pour de triple émission éntière, 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]]
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications.
Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que dupliquer l'ALU entière peut se marier parfaitement avec la présence d'une FPU. Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU reconditionne sa FPU ou pas. Mais il est très rare qu'un CPU ait à la fois un multiplieur entier et une FPU reconditionnée.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires modernes utilisent toutes ces stratégies===
Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, duplication de l'ALU entière, duplication des autre unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents.
* Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU.
* Le '''reconditionnement des unités''' a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU.
* La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants.
* La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables.
De nos jours, le partitionnement est utilisé en complément de la duplication des ALU entières, et de la duplication des autres unités. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993.
Le meilleur moyen de voir comment sont combinés ces quatre technique est de regarder ce qu'il s'est passé historiquement.
==Les processeurs implémentés avec plusieurs pipelines==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Vous vous dites que l'implémentation d'un processeur superscalaire n'est pas la même suivant que le CPU utilise l'exécution dans le désordre, ou non. Mais dans les faits, la distinction est plus subtile que ca. Pour l'expliquer, je vais utiliser une classification originale, qui sépare les processeurs superscalaires en trois sous-types. La distinction se fait sur la manière dont la superscalarité est implémentée en matériel.La classification que j'utilise distingue :
* Les processeurs avec plusieurs pipelines, sans exécution dans le désordre.
* Les processeurs superscalaires avec un pipeline dynamique.
* Les processeurs avec des fenêtres d'instruction décentralisées.
La distinction n'est pas qu'une question d'implémentation matérielle. Elle impacte aussi le ratio entre instructions chargées/décodées et instructions émises. Vous vous dites que les deux sont égales, mais ce n'est le cas que sur les processeurs avec N pipelines ! Sur les autres processeurs, il est possible de décoder 4 instructions, mais d'en émettre maximum 6 par cycle ! Et sur les CPU avec des fenêtres d'instruction décentralisées, il y a aussi une différence entre le nombre d'instructions qui sont ''dispatch'' et ''schedule''.
La catégorie la plus simple est celle des '''processeurs avec plusieurs pipelines'''. Ce sont des processeurs superscalaires sans exécution dans le désordre. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction.
Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, d'une FPU, éventuellement d'un second banc de registres, guère plus. Il permet d'exécuter une seconde opération de calcul, pas plus, ce qui est à l'origine de contraintes d'appariemment assez fortes. De plus, il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section.
===La double émission entière-flottante===
L'exemple le plus simple utilise un pipeline pour les opérations flottantes, séparé du pipeline originel. L'implémentation est assez simple : la FPU et le banc de registre flottant sont détachés du pipeline originel, et deviennent un pipeline séparé à eux tout seuls. Pour bien expliquer les choses, prenons un processeur avec un pipeline unique, tel qu'illustré ci-dessous. Les instructions entières et mémoire passent toutes par l'ALU entière avant d’atterrir dans l'unité mémoire, pour ensuite arriver à l'étape de ''writeback''. La FPU est un second ''back-end'', mais cela ne veut pas dire fait elle aussi une partie du pipeline, même si ce n'est pas évident.
{|
|[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU.]]
|[[File:CPU non-superscalaire basique, avec une FPU.png|thumb|CPU non-superscalaire basique, avec une FPU]]
|}
L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
===La double émission entière===
La seconde classe de CPU superscalaires de l'époque utilisait la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Pour cela, ils peuvent dupliquer une ALU entière, ou reconditionner l'unité mémoire. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 reconditionnait l'unité mémoire en ALU entière.
L'Intel i960 utilisait le reconditionnement de l'AGU en plus du partitionnement, ce qui lui permettait d'exécuter une seconde instruction entière, en plus d'une autre instruction. Les instructions en question étaient simples : additions, quelques décalages. De nos jours, ce combo partitionnement et reconditionnement de l'AGU n'est plus utilisé. Il n'a pas beaucoup de sens quand on peut dupliquer des ALU entières, et les processeurs récents ont un budget en transistor suffisant pour.
Le Pentium dupliquait l'ALU entière, mais n'utilisaient aucune forme de partitionnement. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Contrairement au i960, le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
==L'évolution historique de la superscalarité==
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. La plupart des CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 sont rapidement montés à de la quadruple émission, avec quelques exceptions. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 ||
|-
! 1995
| 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000
|-
! 1996
| class="f_bleu | 620 || || || || class="f_bleu | 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
===Les processeurs historiques à double émission===
Pour commencer, étudions le cas des premiers CPU superscalaires à double émission. Ils sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Notamment, la duplication des autres unités ne sert pour ainsi dire à rien, ce qui limite les possibilités, mais rendra les explications plus simples.
Il pouvaient se classer en deux/trois types principaux, assez stricts, avec quelques processeurs qui ne rentraient pas dans ces cases. Le premier utilise le partitionnement seul, le second dupliquer les ALU entières sans partitionnement, et le troisième type est volontairement passé sous silence pour le moment (nous aborderons le troisième type dans la partie sur des design à quadruple émission).
Les CPU n'utilisant que le partitionnement faisaient la double émission avec trois unités : une ALU, une FPU et une unité mémoire. Le reconditionnement de la FPU était parfois utilisé, mais l'AGU n'était jamais reconditionnée. Si la moitié des processeurs faisait du ''partitionnement complet'' entre ALU, FPU et unité mémoire, l'autre moitié utilisait la bouble émission entière-flottante.
Pour résumer, voici le tableau chronologique disant quel CPU utilisait quelle technique.
* Le partitionnement INT-FLOAT-MEM est en jaune.
* La double émission entière-flottante est en vert.
* La double émission entière est en rouge.
* Les CPU barrés utilisent la quadruple émission et seront vu dans la section suivante.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| class="f_vert | RS/6000 || || || class="f_rouge | i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| class="f_vert | RSC || class="f_vert | PA 7100 || SuperSPARC || || class="f_jaune | 21064 ||
|-
! 1993
| class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || ||
|-
! 1994
| <s>POWER 2</s> || PA 7200 || || || <s>21164</s> ||
|}
Vous voyez que certains processeur sont laissés de côté dans le tableau précédent. C'est car ils ne rentrent pas dans les catégories précédentes et faisaient autrement. Si on omet le Motorola 88110, qui utilisait une forme de double émission assez avancée, les deux CPUs qui vont nous intéresser sont le PA-RISC PA 7200 d'HP, et le SuperSPARC.
Le PA 7200, sorti en 1994, est un PA-7100 auquel on aurait rajouté une ALU entière. Précisons que ce processeur utilisait aussi le reconditionnement de la FPU, comme son prédécesseur. Les combinaisons autorisées étaient donc INT + INT, INT + MEM, INT + FLOAT/MUL, MEM + FLOAT/MUL. Les combinaisons autorisées par le partitionnement INT-FLOAT-MEM étaient donc possibles, en plus des combinaisons INT + INT, voire INT + INT + MUL (vu que la FPU est reconditionnée). Autrement dit, il pouvait faire toute ce que permettait un CPU à double émission entière, plus les combinaisons FLOAT/MUL + MEM.
Le SuperSPARC était un CPU triple émission, avec deux ALU entières, une FPU et une unité mémoire. Il pouvait émettre, à chaque cycle, trois µops en piochant dans deux opérations entières, un accès mémoire, une opération flottante. Le SuperSPARC avait de nombreuses contraintes d’appariement. Par exemple, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Une bonne partie de ces contraintes était lié au banc de registre, qui n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions.
: La seconde ALU a été abandonnée dans l'HyperSPARC, qui est de plus repassé en double émission, ce qui est une évolution à rebours de l'évolution des autres CPUs. Le SuperSPARC était en avance sur son temps, ce quelques années, alors que l'HyperSPARC a un design de son temps.
===Les processeurs historiques à quadruple émission===
C'est la même année , en 1994, que la quadruple émission est arrivée et que les CPU ont commencé à avoir de plus en plus d'unités de calcul. Même les designs restés en double émission avaient ajouté des unités de calcul. Il y avait alors trois stratégies pour la quadruple émission.
La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Les FPUs étaient soit dupliquées, soit scindées en deux en séparant l'additionneur du multiplieur flottant. Le POWER 2 dupliquait ses FPUs à la perfection, alors que l'21264 partitionnait la FPU. Les accès mémoire étaient traités comme des opérations entières, et le processeur pouvait : soit en faire un seul par cycle, soit en émettre deux, sous conditions (le cache devait être double port pour lire/écrire deux données à la fois, comme on le verra dans la suite du chapitre).
Les processeurs MIPS R8000 et R10000 allaient un peu plus loin, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités.
La seconde implémentation dupliquait l'ALU entière et combinait le tout avec un partitionnement INT-FLOAT-MEM. De quoi exécuter en même temps deux opérations entières, une opération flottante, et un accès mémoire. 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).
Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, avec au minimum deux ALU entières. Pour comprendre ce que je veux dire, prenons l'exemple de l'UltraSPARC. Il avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplier flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes. Le processeur pouvait émettre 4 instructions, tant qu'elles atterrissaient dans des unités différentes.
La même méthode peut être utilisée pour la double émission. Le Motorola 88110, sorti en 1991, était un CPU à double émission avec pas moins de 10 unités : deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise.
La morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes. Le tout est résumé dans le tableau ci-dessous, avec :
* en vert les CPU utilisant la première stratégie ;
* en rouge les CPU utilisant la seconde stratégie ;
* en jaune les CPU utilisant la troisième stratégie.
{|class="wikitable"
|+ Processeurs quadruple émission historiques
|-
! Année !! Processeur !! Unités
|-
! 1991
| class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques
|-
|-
! rowspan="2" | 1994
| class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'')
|-
| class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM
|-
! rowspan="3" | 1995
| class="f_jaune | PA 8000 || class="f_jaune | 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD
|-
| class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques
|-
| MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM
|-
! rowspan="2" | 1996
| class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM
|-
| class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL)
|}
===Les processeurs superscalaires modernes===
Les CPU superscalaires modernes ont des design légèrement différents des CPU historiques. Ils dupliquent l'unité entière, mais leur partitionnement est différent. En général, les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Par exemple, un CPU à 6 voies pourra émettre/exécuter 6 opérations entières simultanément, grâce à 6 ALU entières. Mais il s'agit là d'un maximum et certaines peuvent être remplacées par une opération flottante ou mémoire, qui utilisent l'unité mémoire et/ou la FPU.
Un exemple assez parlant est les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon. C'était une microarchitecture à triple émission, avec trois ALU entières et trois unités mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements.
Il s'agit là d'une tendance, cependant. Les CPU superscalaires tendent à mettre le paquet niveau opérations arithmétiques, mais pas forcément au point de remplir la totalité de la largeur du processeur. La règle est que pour N voies, le processeur dispose d'au moins N/2 ALU entières. Attention, cela ne signifie pas que le processeur émet systématiquement N/2 opération entières. Il peut par exemple émettre une combinaison d'opérations flottantes et mémoire, avec des branchements. Et N/2 est une borne basse, la plupart des CPU x86 ont environ tournent plutôt autour des 2/3, voire des 3/4.
En général, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. La conséquence est que les unités mémoire sont dupliquées, en seconde place dans l'ordre d'importance. Les processeurs x86 ont commencé à dupliquer leurs unités mémoire dès qu'ils ont atteint la quadruple émission, pour les CPU grand public. La microarchitecture Core d'Intel utilisait la quadruple émission, avec deux unités mémoire et trois ALU entières. Les microarchitectures d'AMD K5 et K6 utilisaient aussi la quadruple émission et avaient deux ALUs pour deux unités mémoire.
Plus récemment, la microarchitecture ''Golden Cove'' dispose de cinq unités de calcul trois unités mémoire (deux spécialisées dans les écritures, trois dans les lectures). La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire.Il y a donc soit égalité, soit avantage pour les ALU entières.
Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair.
==L'unité d'émission d'un processeur superscalaire==
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Voyons pourquoi.
===L'excès de ports d'émission===
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ou 5 ?
Une solution est de réduire la largeur du processeur. Dans l'exemple précédent, il est possible de passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La solution précédente est certes très intéressante, mais elle a des défauts. Le principal est que les ports d'émission sont sous-utilisés. Et les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
===L'intérieur de l'unité d'émission===
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Le banc de registre d'un processeur superscalaire===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Une autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions auto-aligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions auto-aligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez longs, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
f9qp71sfl9u980h45v51mjzhk8z9vyp
773140
773139
2026-09-25T18:44:40Z
Mewtow
31375
/* Les processeurs implémentés avec plusieurs pipelines */
773140
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===L'impact des contraintes d’appariement sur la performance===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour la suite du chapitre===
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==Le ''front-end'' des processeurs superscalaires==
Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple.
Intuitivement, charger, décoder et 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 certaines processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. 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
|}
Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs.
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
==Les unités de calcul d'un processeur superscalaire==
Après avoir vu le chargement et le décodage superscalaire, voyons ce qu'il en est au niveau des unités de calcul. Nous allons parler des unités de calcul avant de parler de l'émission, pour une raison simple : l'émission fait l'interface entre unité de calcul et ''front-end'' et on ne peut pas parler de l'émission sans avoir vu comment la superscalarité impacte les deux. Si le ''front-end'' charge et décode un paquet de N instructions, son exécution demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est possible d'exploiter plusieurs unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu.
Les tout premiers CPU superscalaires étaient nombreux à utiliser cette technique. Ils étaient des CPU double émission, avec une ALU entière, une unité mémoire et une FPU. Nous parlerons de '''partitionnement INT-FLOAT-MEM'''. Il s'agit de l'HyperSPARC, de l'alpha 21064 et du PowerPC 603. Les contrainte d’appariement variaient grandement suivant le processeur. Les plus permissifs autorisaient toutes les combinaisons possibles entre : une opération entière, une opération flottante, et une unité mémoire. Mais ils n'étaient pas les seuls.
Le PowerPC 603 implémentait ce genre de double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions).
Il est aussi possible de faire la même chose pour les opérations entières, vu qu'elles sont prises en charge par trois circuits. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres. Il faut parfois lui ajouter le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible d'émettre une opération séparément, ce qui donne le '''partitionnement entier''' dans ce qui suit. Le partitionnement entier n'est jamais utilisé tel quel, car le gain en performance serait trop faible au regard des couts. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Le '''reconditionnement d'une unité fonctionnelle''' permet à celle-ci de faire des opérations qu'elle n'est pas prévue pour. Par exemple, il est possible d'utiliser l'unité mémoire comme seconde ALU entière, ou d'utiliser la FPU comme multiplieur. Ces deux exemples ne sont pas choisit au hasard, nous les étudierons dans ce qui suit. Le reconditionnement évite d'avoir à dupliquer une unité de calcul, tout en ajoutant une voie. Il permet d'améliorer l'efficacité du partitionnement, qu'il soit utilisé seul ou non.
Il est possible de reconditionner la FPU pour faire des multiplications entières. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits. Il est possible d'utiliser de multiplieur pour faire des multiplications entières 32 bits. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
Le reconditionnement de la FPU permet d'exécuter une multiplication entière en plus des autres opérations entières. Par exemple, prenons un CPU avec double émission entière-flottante, sans multiplieur entier séparé. Le reconditionnement de la FPU permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine.
Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants.
Passons maintenant à une seconde forme de reconditionnement. Reconditionner l'unité mémoire permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, des versions améliorées de l'Intel 960. L'Intel 960 originel n'était pas superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire au monde. Il intégrait une ALU entière, une unité mémoire et une unité de branchement. Grâce au partitionnement, il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, en remplacement d'une µop mémoire. Son pipeline permettait donc les combinaisons INT + BRANCH + (MEM / + INT*).
Le cas de l'i960 nous montre que le reconditionnement permet d'améliorer l'intérêt du partitionnement. Elle permet de ne pas dupliquer d'unité de calcul, tout en permettant quand même d'émettre une seconde opération entière.
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''.
Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières ! Il doit y avoir deux ALU entières pour de la double émission entière, trois pour de triple émission éntière, 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]]
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications.
Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que dupliquer l'ALU entière peut se marier parfaitement avec la présence d'une FPU. Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU reconditionne sa FPU ou pas. Mais il est très rare qu'un CPU ait à la fois un multiplieur entier et une FPU reconditionnée.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires modernes utilisent toutes ces stratégies===
Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, duplication de l'ALU entière, duplication des autre unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents.
* Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU.
* Le '''reconditionnement des unités''' a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU.
* La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants.
* La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables.
De nos jours, le partitionnement est utilisé en complément de la duplication des ALU entières, et de la duplication des autres unités. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993.
Le meilleur moyen de voir comment sont combinés ces quatre technique est de regarder ce qu'il s'est passé historiquement.
==L'évolution historique de la superscalarité==
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. La plupart des CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 sont rapidement montés à de la quadruple émission, avec quelques exceptions. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 ||
|-
! 1995
| 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000
|-
! 1996
| class="f_bleu | 620 || || || || class="f_bleu | 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
===Les processeurs historiques à double émission===
Pour commencer, étudions le cas des premiers CPU superscalaires à double émission. Ils sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Notamment, la duplication des autres unités ne sert pour ainsi dire à rien, ce qui limite les possibilités, mais rendra les explications plus simples.
Il pouvaient se classer en deux/trois types principaux, assez stricts, avec quelques processeurs qui ne rentraient pas dans ces cases. Le premier utilise le partitionnement seul, le second dupliquer les ALU entières sans partitionnement, et le troisième type est volontairement passé sous silence pour le moment (nous aborderons le troisième type dans la partie sur des design à quadruple émission).
Les CPU n'utilisant que le partitionnement faisaient la double émission avec trois unités : une ALU, une FPU et une unité mémoire. Le reconditionnement de la FPU était parfois utilisé, mais l'AGU n'était jamais reconditionnée. Si la moitié des processeurs faisait du ''partitionnement complet'' entre ALU, FPU et unité mémoire, l'autre moitié utilisait la bouble émission entière-flottante.
Pour résumer, voici le tableau chronologique disant quel CPU utilisait quelle technique.
* Le partitionnement INT-FLOAT-MEM est en jaune.
* La double émission entière-flottante est en vert.
* La double émission entière est en rouge.
* Les CPU barrés utilisent la quadruple émission et seront vu dans la section suivante.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| class="f_vert | RS/6000 || || || class="f_rouge | i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| class="f_vert | RSC || class="f_vert | PA 7100 || SuperSPARC || || class="f_jaune | 21064 ||
|-
! 1993
| class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || ||
|-
! 1994
| <s>POWER 2</s> || PA 7200 || || || <s>21164</s> ||
|}
Vous voyez que certains processeur sont laissés de côté dans le tableau précédent. C'est car ils ne rentrent pas dans les catégories précédentes et faisaient autrement. Si on omet le Motorola 88110, qui utilisait une forme de double émission assez avancée, les deux CPUs qui vont nous intéresser sont le PA-RISC PA 7200 d'HP, et le SuperSPARC.
Le PA 7200, sorti en 1994, est un PA-7100 auquel on aurait rajouté une ALU entière. Précisons que ce processeur utilisait aussi le reconditionnement de la FPU, comme son prédécesseur. Les combinaisons autorisées étaient donc INT + INT, INT + MEM, INT + FLOAT/MUL, MEM + FLOAT/MUL. Les combinaisons autorisées par le partitionnement INT-FLOAT-MEM étaient donc possibles, en plus des combinaisons INT + INT, voire INT + INT + MUL (vu que la FPU est reconditionnée). Autrement dit, il pouvait faire toute ce que permettait un CPU à double émission entière, plus les combinaisons FLOAT/MUL + MEM.
Le SuperSPARC était un CPU triple émission, avec deux ALU entières, une FPU et une unité mémoire. Il pouvait émettre, à chaque cycle, trois µops en piochant dans deux opérations entières, un accès mémoire, une opération flottante. Le SuperSPARC avait de nombreuses contraintes d’appariement. Par exemple, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Une bonne partie de ces contraintes était lié au banc de registre, qui n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions.
: La seconde ALU a été abandonnée dans l'HyperSPARC, qui est de plus repassé en double émission, ce qui est une évolution à rebours de l'évolution des autres CPUs. Le SuperSPARC était en avance sur son temps, ce quelques années, alors que l'HyperSPARC a un design de son temps.
===Les processeurs historiques à quadruple émission===
C'est la même année , en 1994, que la quadruple émission est arrivée et que les CPU ont commencé à avoir de plus en plus d'unités de calcul. Même les designs restés en double émission avaient ajouté des unités de calcul. Il y avait alors trois stratégies pour la quadruple émission.
La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Les FPUs étaient soit dupliquées, soit scindées en deux en séparant l'additionneur du multiplieur flottant. Le POWER 2 dupliquait ses FPUs à la perfection, alors que l'21264 partitionnait la FPU. Les accès mémoire étaient traités comme des opérations entières, et le processeur pouvait : soit en faire un seul par cycle, soit en émettre deux, sous conditions (le cache devait être double port pour lire/écrire deux données à la fois, comme on le verra dans la suite du chapitre).
Les processeurs MIPS R8000 et R10000 allaient un peu plus loin, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités.
La seconde implémentation dupliquait l'ALU entière et combinait le tout avec un partitionnement INT-FLOAT-MEM. De quoi exécuter en même temps deux opérations entières, une opération flottante, et un accès mémoire. 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).
Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, avec au minimum deux ALU entières. Pour comprendre ce que je veux dire, prenons l'exemple de l'UltraSPARC. Il avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplier flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes. Le processeur pouvait émettre 4 instructions, tant qu'elles atterrissaient dans des unités différentes.
La même méthode peut être utilisée pour la double émission. Le Motorola 88110, sorti en 1991, était un CPU à double émission avec pas moins de 10 unités : deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise.
La morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes. Le tout est résumé dans le tableau ci-dessous, avec :
* en vert les CPU utilisant la première stratégie ;
* en rouge les CPU utilisant la seconde stratégie ;
* en jaune les CPU utilisant la troisième stratégie.
{|class="wikitable"
|+ Processeurs quadruple émission historiques
|-
! Année !! Processeur !! Unités
|-
! 1991
| class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques
|-
|-
! rowspan="2" | 1994
| class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'')
|-
| class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM
|-
! rowspan="3" | 1995
| class="f_jaune | PA 8000 || class="f_jaune | 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD
|-
| class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques
|-
| MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM
|-
! rowspan="2" | 1996
| class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM
|-
| class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL)
|}
===Les processeurs superscalaires modernes===
Les CPU superscalaires modernes ont des design légèrement différents des CPU historiques. Ils dupliquent l'unité entière, mais leur partitionnement est différent. En général, les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Par exemple, un CPU à 6 voies pourra émettre/exécuter 6 opérations entières simultanément, grâce à 6 ALU entières. Mais il s'agit là d'un maximum et certaines peuvent être remplacées par une opération flottante ou mémoire, qui utilisent l'unité mémoire et/ou la FPU.
Un exemple assez parlant est les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon. C'était une microarchitecture à triple émission, avec trois ALU entières et trois unités mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements.
Il s'agit là d'une tendance, cependant. Les CPU superscalaires tendent à mettre le paquet niveau opérations arithmétiques, mais pas forcément au point de remplir la totalité de la largeur du processeur. La règle est que pour N voies, le processeur dispose d'au moins N/2 ALU entières. Attention, cela ne signifie pas que le processeur émet systématiquement N/2 opération entières. Il peut par exemple émettre une combinaison d'opérations flottantes et mémoire, avec des branchements. Et N/2 est une borne basse, la plupart des CPU x86 ont environ tournent plutôt autour des 2/3, voire des 3/4.
En général, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. La conséquence est que les unités mémoire sont dupliquées, en seconde place dans l'ordre d'importance. Les processeurs x86 ont commencé à dupliquer leurs unités mémoire dès qu'ils ont atteint la quadruple émission, pour les CPU grand public. La microarchitecture Core d'Intel utilisait la quadruple émission, avec deux unités mémoire et trois ALU entières. Les microarchitectures d'AMD K5 et K6 utilisaient aussi la quadruple émission et avaient deux ALUs pour deux unités mémoire.
Plus récemment, la microarchitecture ''Golden Cove'' dispose de cinq unités de calcul trois unités mémoire (deux spécialisées dans les écritures, trois dans les lectures). La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire.Il y a donc soit égalité, soit avantage pour les ALU entières.
Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair.
==L'unité d'émission d'un processeur superscalaire==
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Voyons pourquoi.
===L'excès de ports d'émission===
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ou 5 ?
Une solution est de réduire la largeur du processeur. Dans l'exemple précédent, il est possible de passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La solution précédente est certes très intéressante, mais elle a des défauts. Le principal est que les ports d'émission sont sous-utilisés. Et les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
===L'intérieur de l'unité d'émission===
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Le banc de registre d'un processeur superscalaire===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Une autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions auto-aligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions auto-aligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez longs, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
rjk29exw9ztwe2cux4resnz05tt7lh5
773141
773140
2026-09-25T18:44:49Z
Mewtow
31375
/* Les unités de calcul d'un processeur superscalaire */
773141
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===L'impact des contraintes d’appariement sur la performance===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour la suite du chapitre===
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==Le ''front-end'' des processeurs superscalaires==
Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple.
Intuitivement, charger, décoder et 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 certaines processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. 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
|}
Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs.
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
==Les processeurs implémentés avec plusieurs pipelines==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Vous vous dites que l'implémentation d'un processeur superscalaire n'est pas la même suivant que le CPU utilise l'exécution dans le désordre, ou non. Mais dans les faits, la distinction est plus subtile que ca. Pour l'expliquer, je vais utiliser une classification originale, qui sépare les processeurs superscalaires en trois sous-types. La distinction se fait sur la manière dont la superscalarité est implémentée en matériel.La classification que j'utilise distingue :
* Les processeurs avec plusieurs pipelines, sans exécution dans le désordre.
* Les processeurs superscalaires avec un pipeline dynamique.
* Les processeurs avec des fenêtres d'instruction décentralisées.
La distinction n'est pas qu'une question d'implémentation matérielle. Elle impacte aussi le ratio entre instructions chargées/décodées et instructions émises. Vous vous dites que les deux sont égales, mais ce n'est le cas que sur les processeurs avec N pipelines ! Sur les autres processeurs, il est possible de décoder 4 instructions, mais d'en émettre maximum 6 par cycle ! Et sur les CPU avec des fenêtres d'instruction décentralisées, il y a aussi une différence entre le nombre d'instructions qui sont ''dispatch'' et ''schedule''.
La catégorie la plus simple est celle des '''processeurs avec plusieurs pipelines'''. Ce sont des processeurs superscalaires sans exécution dans le désordre. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction.
Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, d'une FPU, éventuellement d'un second banc de registres, guère plus. Il permet d'exécuter une seconde opération de calcul, pas plus, ce qui est à l'origine de contraintes d'appariemment assez fortes. De plus, il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section.
===La double émission entière-flottante===
L'exemple le plus simple utilise un pipeline pour les opérations flottantes, séparé du pipeline originel. L'implémentation est assez simple : la FPU et le banc de registre flottant sont détachés du pipeline originel, et deviennent un pipeline séparé à eux tout seuls. Pour bien expliquer les choses, prenons un processeur avec un pipeline unique, tel qu'illustré ci-dessous. Les instructions entières et mémoire passent toutes par l'ALU entière avant d’atterrir dans l'unité mémoire, pour ensuite arriver à l'étape de ''writeback''. La FPU est un second ''back-end'', mais cela ne veut pas dire fait elle aussi une partie du pipeline, même si ce n'est pas évident.
{|
|[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU.]]
|[[File:CPU non-superscalaire basique, avec une FPU.png|thumb|CPU non-superscalaire basique, avec une FPU]]
|}
L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
===La double émission entière===
La seconde classe de CPU superscalaires de l'époque utilisait la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Pour cela, ils peuvent dupliquer une ALU entière, ou reconditionner l'unité mémoire. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 reconditionnait l'unité mémoire en ALU entière.
L'Intel i960 utilisait le reconditionnement de l'AGU en plus du partitionnement, ce qui lui permettait d'exécuter une seconde instruction entière, en plus d'une autre instruction. Les instructions en question étaient simples : additions, quelques décalages. De nos jours, ce combo partitionnement et reconditionnement de l'AGU n'est plus utilisé. Il n'a pas beaucoup de sens quand on peut dupliquer des ALU entières, et les processeurs récents ont un budget en transistor suffisant pour.
Le Pentium dupliquait l'ALU entière, mais n'utilisaient aucune forme de partitionnement. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Contrairement au i960, le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
==Les unités de calcul d'un processeur superscalaire==
Après avoir vu le chargement et le décodage superscalaire, voyons ce qu'il en est au niveau des unités de calcul. Nous allons parler des unités de calcul avant de parler de l'émission, pour une raison simple : l'émission fait l'interface entre unité de calcul et ''front-end'' et on ne peut pas parler de l'émission sans avoir vu comment la superscalarité impacte les deux. Si le ''front-end'' charge et décode un paquet de N instructions, son exécution demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est possible d'exploiter plusieurs unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu.
Les tout premiers CPU superscalaires étaient nombreux à utiliser cette technique. Ils étaient des CPU double émission, avec une ALU entière, une unité mémoire et une FPU. Nous parlerons de '''partitionnement INT-FLOAT-MEM'''. Il s'agit de l'HyperSPARC, de l'alpha 21064 et du PowerPC 603. Les contrainte d’appariement variaient grandement suivant le processeur. Les plus permissifs autorisaient toutes les combinaisons possibles entre : une opération entière, une opération flottante, et une unité mémoire. Mais ils n'étaient pas les seuls.
Le PowerPC 603 implémentait ce genre de double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions).
Il est aussi possible de faire la même chose pour les opérations entières, vu qu'elles sont prises en charge par trois circuits. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres. Il faut parfois lui ajouter le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible d'émettre une opération séparément, ce qui donne le '''partitionnement entier''' dans ce qui suit. Le partitionnement entier n'est jamais utilisé tel quel, car le gain en performance serait trop faible au regard des couts. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Le '''reconditionnement d'une unité fonctionnelle''' permet à celle-ci de faire des opérations qu'elle n'est pas prévue pour. Par exemple, il est possible d'utiliser l'unité mémoire comme seconde ALU entière, ou d'utiliser la FPU comme multiplieur. Ces deux exemples ne sont pas choisit au hasard, nous les étudierons dans ce qui suit. Le reconditionnement évite d'avoir à dupliquer une unité de calcul, tout en ajoutant une voie. Il permet d'améliorer l'efficacité du partitionnement, qu'il soit utilisé seul ou non.
Il est possible de reconditionner la FPU pour faire des multiplications entières. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits. Il est possible d'utiliser de multiplieur pour faire des multiplications entières 32 bits. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
Le reconditionnement de la FPU permet d'exécuter une multiplication entière en plus des autres opérations entières. Par exemple, prenons un CPU avec double émission entière-flottante, sans multiplieur entier séparé. Le reconditionnement de la FPU permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine.
Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants.
Passons maintenant à une seconde forme de reconditionnement. Reconditionner l'unité mémoire permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, des versions améliorées de l'Intel 960. L'Intel 960 originel n'était pas superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire au monde. Il intégrait une ALU entière, une unité mémoire et une unité de branchement. Grâce au partitionnement, il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, en remplacement d'une µop mémoire. Son pipeline permettait donc les combinaisons INT + BRANCH + (MEM / + INT*).
Le cas de l'i960 nous montre que le reconditionnement permet d'améliorer l'intérêt du partitionnement. Elle permet de ne pas dupliquer d'unité de calcul, tout en permettant quand même d'émettre une seconde opération entière.
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''.
Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières ! Il doit y avoir deux ALU entières pour de la double émission entière, trois pour de triple émission éntière, 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]]
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications.
Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que dupliquer l'ALU entière peut se marier parfaitement avec la présence d'une FPU. Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU reconditionne sa FPU ou pas. Mais il est très rare qu'un CPU ait à la fois un multiplieur entier et une FPU reconditionnée.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires modernes utilisent toutes ces stratégies===
Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, duplication de l'ALU entière, duplication des autre unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents.
* Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU.
* Le '''reconditionnement des unités''' a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU.
* La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants.
* La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables.
De nos jours, le partitionnement est utilisé en complément de la duplication des ALU entières, et de la duplication des autres unités. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993.
Le meilleur moyen de voir comment sont combinés ces quatre technique est de regarder ce qu'il s'est passé historiquement.
==L'évolution historique de la superscalarité==
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. La plupart des CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 sont rapidement montés à de la quadruple émission, avec quelques exceptions. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 ||
|-
! 1995
| 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000
|-
! 1996
| class="f_bleu | 620 || || || || class="f_bleu | 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
===Les processeurs historiques à double émission===
Pour commencer, étudions le cas des premiers CPU superscalaires à double émission. Ils sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Notamment, la duplication des autres unités ne sert pour ainsi dire à rien, ce qui limite les possibilités, mais rendra les explications plus simples.
Il pouvaient se classer en deux/trois types principaux, assez stricts, avec quelques processeurs qui ne rentraient pas dans ces cases. Le premier utilise le partitionnement seul, le second dupliquer les ALU entières sans partitionnement, et le troisième type est volontairement passé sous silence pour le moment (nous aborderons le troisième type dans la partie sur des design à quadruple émission).
Les CPU n'utilisant que le partitionnement faisaient la double émission avec trois unités : une ALU, une FPU et une unité mémoire. Le reconditionnement de la FPU était parfois utilisé, mais l'AGU n'était jamais reconditionnée. Si la moitié des processeurs faisait du ''partitionnement complet'' entre ALU, FPU et unité mémoire, l'autre moitié utilisait la bouble émission entière-flottante.
Pour résumer, voici le tableau chronologique disant quel CPU utilisait quelle technique.
* Le partitionnement INT-FLOAT-MEM est en jaune.
* La double émission entière-flottante est en vert.
* La double émission entière est en rouge.
* Les CPU barrés utilisent la quadruple émission et seront vu dans la section suivante.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| class="f_vert | RS/6000 || || || class="f_rouge | i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| class="f_vert | RSC || class="f_vert | PA 7100 || SuperSPARC || || class="f_jaune | 21064 ||
|-
! 1993
| class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || ||
|-
! 1994
| <s>POWER 2</s> || PA 7200 || || || <s>21164</s> ||
|}
Vous voyez que certains processeur sont laissés de côté dans le tableau précédent. C'est car ils ne rentrent pas dans les catégories précédentes et faisaient autrement. Si on omet le Motorola 88110, qui utilisait une forme de double émission assez avancée, les deux CPUs qui vont nous intéresser sont le PA-RISC PA 7200 d'HP, et le SuperSPARC.
Le PA 7200, sorti en 1994, est un PA-7100 auquel on aurait rajouté une ALU entière. Précisons que ce processeur utilisait aussi le reconditionnement de la FPU, comme son prédécesseur. Les combinaisons autorisées étaient donc INT + INT, INT + MEM, INT + FLOAT/MUL, MEM + FLOAT/MUL. Les combinaisons autorisées par le partitionnement INT-FLOAT-MEM étaient donc possibles, en plus des combinaisons INT + INT, voire INT + INT + MUL (vu que la FPU est reconditionnée). Autrement dit, il pouvait faire toute ce que permettait un CPU à double émission entière, plus les combinaisons FLOAT/MUL + MEM.
Le SuperSPARC était un CPU triple émission, avec deux ALU entières, une FPU et une unité mémoire. Il pouvait émettre, à chaque cycle, trois µops en piochant dans deux opérations entières, un accès mémoire, une opération flottante. Le SuperSPARC avait de nombreuses contraintes d’appariement. Par exemple, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Une bonne partie de ces contraintes était lié au banc de registre, qui n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions.
: La seconde ALU a été abandonnée dans l'HyperSPARC, qui est de plus repassé en double émission, ce qui est une évolution à rebours de l'évolution des autres CPUs. Le SuperSPARC était en avance sur son temps, ce quelques années, alors que l'HyperSPARC a un design de son temps.
===Les processeurs historiques à quadruple émission===
C'est la même année , en 1994, que la quadruple émission est arrivée et que les CPU ont commencé à avoir de plus en plus d'unités de calcul. Même les designs restés en double émission avaient ajouté des unités de calcul. Il y avait alors trois stratégies pour la quadruple émission.
La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Les FPUs étaient soit dupliquées, soit scindées en deux en séparant l'additionneur du multiplieur flottant. Le POWER 2 dupliquait ses FPUs à la perfection, alors que l'21264 partitionnait la FPU. Les accès mémoire étaient traités comme des opérations entières, et le processeur pouvait : soit en faire un seul par cycle, soit en émettre deux, sous conditions (le cache devait être double port pour lire/écrire deux données à la fois, comme on le verra dans la suite du chapitre).
Les processeurs MIPS R8000 et R10000 allaient un peu plus loin, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités.
La seconde implémentation dupliquait l'ALU entière et combinait le tout avec un partitionnement INT-FLOAT-MEM. De quoi exécuter en même temps deux opérations entières, une opération flottante, et un accès mémoire. 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).
Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, avec au minimum deux ALU entières. Pour comprendre ce que je veux dire, prenons l'exemple de l'UltraSPARC. Il avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplier flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes. Le processeur pouvait émettre 4 instructions, tant qu'elles atterrissaient dans des unités différentes.
La même méthode peut être utilisée pour la double émission. Le Motorola 88110, sorti en 1991, était un CPU à double émission avec pas moins de 10 unités : deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise.
La morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes. Le tout est résumé dans le tableau ci-dessous, avec :
* en vert les CPU utilisant la première stratégie ;
* en rouge les CPU utilisant la seconde stratégie ;
* en jaune les CPU utilisant la troisième stratégie.
{|class="wikitable"
|+ Processeurs quadruple émission historiques
|-
! Année !! Processeur !! Unités
|-
! 1991
| class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques
|-
|-
! rowspan="2" | 1994
| class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'')
|-
| class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM
|-
! rowspan="3" | 1995
| class="f_jaune | PA 8000 || class="f_jaune | 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD
|-
| class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques
|-
| MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM
|-
! rowspan="2" | 1996
| class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM
|-
| class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL)
|}
===Les processeurs superscalaires modernes===
Les CPU superscalaires modernes ont des design légèrement différents des CPU historiques. Ils dupliquent l'unité entière, mais leur partitionnement est différent. En général, les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Par exemple, un CPU à 6 voies pourra émettre/exécuter 6 opérations entières simultanément, grâce à 6 ALU entières. Mais il s'agit là d'un maximum et certaines peuvent être remplacées par une opération flottante ou mémoire, qui utilisent l'unité mémoire et/ou la FPU.
Un exemple assez parlant est les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon. C'était une microarchitecture à triple émission, avec trois ALU entières et trois unités mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements.
Il s'agit là d'une tendance, cependant. Les CPU superscalaires tendent à mettre le paquet niveau opérations arithmétiques, mais pas forcément au point de remplir la totalité de la largeur du processeur. La règle est que pour N voies, le processeur dispose d'au moins N/2 ALU entières. Attention, cela ne signifie pas que le processeur émet systématiquement N/2 opération entières. Il peut par exemple émettre une combinaison d'opérations flottantes et mémoire, avec des branchements. Et N/2 est une borne basse, la plupart des CPU x86 ont environ tournent plutôt autour des 2/3, voire des 3/4.
En général, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. La conséquence est que les unités mémoire sont dupliquées, en seconde place dans l'ordre d'importance. Les processeurs x86 ont commencé à dupliquer leurs unités mémoire dès qu'ils ont atteint la quadruple émission, pour les CPU grand public. La microarchitecture Core d'Intel utilisait la quadruple émission, avec deux unités mémoire et trois ALU entières. Les microarchitectures d'AMD K5 et K6 utilisaient aussi la quadruple émission et avaient deux ALUs pour deux unités mémoire.
Plus récemment, la microarchitecture ''Golden Cove'' dispose de cinq unités de calcul trois unités mémoire (deux spécialisées dans les écritures, trois dans les lectures). La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire.Il y a donc soit égalité, soit avantage pour les ALU entières.
Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair.
==L'unité d'émission d'un processeur superscalaire==
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Voyons pourquoi.
===L'excès de ports d'émission===
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ou 5 ?
Une solution est de réduire la largeur du processeur. Dans l'exemple précédent, il est possible de passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La solution précédente est certes très intéressante, mais elle a des défauts. Le principal est que les ports d'émission sont sous-utilisés. Et les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
===L'intérieur de l'unité d'émission===
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Le banc de registre d'un processeur superscalaire===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Une autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions auto-aligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions auto-aligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez longs, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
96vv337pug9x7uwazgflvp1pzj1ov57
773142
773141
2026-09-25T18:45:45Z
Mewtow
31375
/* Les processeurs implémentés avec plusieurs pipelines */
773142
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===L'impact des contraintes d’appariement sur la performance===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour la suite du chapitre===
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==Le ''front-end'' des processeurs superscalaires==
Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple.
Intuitivement, charger, décoder et 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 certaines processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. 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
|}
Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs.
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
==Les processeurs implémentés avec plusieurs pipelines==
Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Vous vous dites que l'implémentation d'un processeur superscalaire n'est pas la même suivant que le CPU utilise l'exécution dans le désordre, ou non. Mais dans les faits, la distinction est plus subtile que ca. Pour l'expliquer, je vais utiliser une classification originale, qui sépare les processeurs superscalaires en trois sous-types. La distinction se fait sur la manière dont la superscalarité est implémentée en matériel.La classification que j'utilise distingue :
* Les processeurs avec plusieurs pipelines, sans exécution dans le désordre.
* Les processeurs superscalaires avec un pipeline dynamique.
* Les processeurs avec des fenêtres d'instruction décentralisées.
La distinction n'est pas qu'une question d'implémentation matérielle. Elle impacte aussi le ratio entre instructions chargées/décodées et instructions émises. Vous vous dites que les deux sont égales, mais ce n'est le cas que sur les processeurs avec N pipelines ! Sur les autres processeurs, il est possible de décoder 4 instructions, mais d'en émettre maximum 6 par cycle ! Et sur les CPU avec des fenêtres d'instruction décentralisées, il y a aussi une différence entre le nombre d'instructions qui sont ''dispatch'' et ''schedule''.
La catégorie la plus simple est celle des '''processeurs avec plusieurs pipelines'''. Ce sont des processeurs superscalaires sans exécution dans le désordre. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction.
Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, d'une FPU, éventuellement d'un second banc de registres, guère plus. Il permet d'exécuter une seconde opération de calcul, pas plus, ce qui est à l'origine de contraintes d'appariemment assez fortes. De plus, il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section.
===La double émission entière-flottante===
L'exemple le plus simple utilise un pipeline pour les opérations flottantes, séparé du pipeline originel. L'implémentation est assez simple : la FPU et le banc de registre flottant sont détachés du pipeline originel, et deviennent un pipeline séparé à eux tout seuls. Pour bien expliquer les choses, prenons un processeur avec un pipeline unique, tel qu'illustré ci-dessous. Les instructions entières et mémoire passent toutes par l'ALU entière avant d’atterrir dans l'unité mémoire, pour ensuite arriver à l'étape de ''writeback''. La FPU est un second ''back-end'', mais cela ne veut pas dire fait elle aussi une partie du pipeline, même si ce n'est pas évident.
{|
|[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU.]]
|[[File:CPU non-superscalaire basique, avec une FPU.png|thumb|CPU non-superscalaire basique, avec une FPU]]
|}
L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
===La double émission entière===
La seconde classe de CPU superscalaires de l'époque utilisait la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Pour cela, ils peuvent dupliquer une ALU entière, ou reconditionner l'unité mémoire. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 reconditionnait l'unité mémoire en ALU entière.
L'Intel i960 utilisait le reconditionnement de l'AGU en plus du partitionnement, ce qui lui permettait d'exécuter une seconde instruction entière, en plus d'une autre instruction. Les instructions en question étaient simples : additions, quelques décalages. De nos jours, ce combo partitionnement et reconditionnement de l'AGU n'est plus utilisé. Il n'a pas beaucoup de sens quand on peut dupliquer des ALU entières, et les processeurs récents ont un budget en transistor suffisant pour.
Le Pentium dupliquait l'ALU entière, mais n'utilisaient aucune forme de partitionnement. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Contrairement au i960, le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
==Les unités de calcul d'un processeur superscalaire==
Après avoir vu le chargement et le décodage superscalaire, voyons ce qu'il en est au niveau des unités de calcul. Nous allons parler des unités de calcul avant de parler de l'émission, pour une raison simple : l'émission fait l'interface entre unité de calcul et ''front-end'' et on ne peut pas parler de l'émission sans avoir vu comment la superscalarité impacte les deux. Si le ''front-end'' charge et décode un paquet de N instructions, son exécution demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est possible d'exploiter plusieurs unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu.
Les tout premiers CPU superscalaires étaient nombreux à utiliser cette technique. Ils étaient des CPU double émission, avec une ALU entière, une unité mémoire et une FPU. Nous parlerons de '''partitionnement INT-FLOAT-MEM'''. Il s'agit de l'HyperSPARC, de l'alpha 21064 et du PowerPC 603. Les contrainte d’appariement variaient grandement suivant le processeur. Les plus permissifs autorisaient toutes les combinaisons possibles entre : une opération entière, une opération flottante, et une unité mémoire. Mais ils n'étaient pas les seuls.
Le PowerPC 603 implémentait ce genre de double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions).
Il est aussi possible de faire la même chose pour les opérations entières, vu qu'elles sont prises en charge par trois circuits. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres. Il faut parfois lui ajouter le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible d'émettre une opération séparément, ce qui donne le '''partitionnement entier''' dans ce qui suit. Le partitionnement entier n'est jamais utilisé tel quel, car le gain en performance serait trop faible au regard des couts. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Le '''reconditionnement d'une unité fonctionnelle''' permet à celle-ci de faire des opérations qu'elle n'est pas prévue pour. Par exemple, il est possible d'utiliser l'unité mémoire comme seconde ALU entière, ou d'utiliser la FPU comme multiplieur. Ces deux exemples ne sont pas choisit au hasard, nous les étudierons dans ce qui suit. Le reconditionnement évite d'avoir à dupliquer une unité de calcul, tout en ajoutant une voie. Il permet d'améliorer l'efficacité du partitionnement, qu'il soit utilisé seul ou non.
Il est possible de reconditionner la FPU pour faire des multiplications entières. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits. Il est possible d'utiliser de multiplieur pour faire des multiplications entières 32 bits. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
Le reconditionnement de la FPU permet d'exécuter une multiplication entière en plus des autres opérations entières. Par exemple, prenons un CPU avec double émission entière-flottante, sans multiplieur entier séparé. Le reconditionnement de la FPU permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine.
Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants.
Passons maintenant à une seconde forme de reconditionnement. Reconditionner l'unité mémoire permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, des versions améliorées de l'Intel 960. L'Intel 960 originel n'était pas superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire au monde. Il intégrait une ALU entière, une unité mémoire et une unité de branchement. Grâce au partitionnement, il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, en remplacement d'une µop mémoire. Son pipeline permettait donc les combinaisons INT + BRANCH + (MEM / + INT*).
Le cas de l'i960 nous montre que le reconditionnement permet d'améliorer l'intérêt du partitionnement. Elle permet de ne pas dupliquer d'unité de calcul, tout en permettant quand même d'émettre une seconde opération entière.
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''.
Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières ! Il doit y avoir deux ALU entières pour de la double émission entière, trois pour de triple émission éntière, 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]]
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications.
Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que dupliquer l'ALU entière peut se marier parfaitement avec la présence d'une FPU. Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU reconditionne sa FPU ou pas. Mais il est très rare qu'un CPU ait à la fois un multiplieur entier et une FPU reconditionnée.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires modernes utilisent toutes ces stratégies===
Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, duplication de l'ALU entière, duplication des autre unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents.
* Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU.
* Le '''reconditionnement des unités''' a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU.
* La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants.
* La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables.
De nos jours, le partitionnement est utilisé en complément de la duplication des ALU entières, et de la duplication des autres unités. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993.
Le meilleur moyen de voir comment sont combinés ces quatre technique est de regarder ce qu'il s'est passé historiquement.
==L'évolution historique de la superscalarité==
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. La plupart des CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 sont rapidement montés à de la quadruple émission, avec quelques exceptions. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 ||
|-
! 1995
| 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000
|-
! 1996
| class="f_bleu | 620 || || || || class="f_bleu | 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
===Les processeurs historiques à double émission===
Pour commencer, étudions le cas des premiers CPU superscalaires à double émission. Ils sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Notamment, la duplication des autres unités ne sert pour ainsi dire à rien, ce qui limite les possibilités, mais rendra les explications plus simples.
Il pouvaient se classer en deux/trois types principaux, assez stricts, avec quelques processeurs qui ne rentraient pas dans ces cases. Le premier utilise le partitionnement seul, le second dupliquer les ALU entières sans partitionnement, et le troisième type est volontairement passé sous silence pour le moment (nous aborderons le troisième type dans la partie sur des design à quadruple émission).
Les CPU n'utilisant que le partitionnement faisaient la double émission avec trois unités : une ALU, une FPU et une unité mémoire. Le reconditionnement de la FPU était parfois utilisé, mais l'AGU n'était jamais reconditionnée. Si la moitié des processeurs faisait du ''partitionnement complet'' entre ALU, FPU et unité mémoire, l'autre moitié utilisait la bouble émission entière-flottante.
Pour résumer, voici le tableau chronologique disant quel CPU utilisait quelle technique.
* Le partitionnement INT-FLOAT-MEM est en jaune.
* La double émission entière-flottante est en vert.
* La double émission entière est en rouge.
* Les CPU barrés utilisent la quadruple émission et seront vu dans la section suivante.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| class="f_vert | RS/6000 || || || class="f_rouge | i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| class="f_vert | RSC || class="f_vert | PA 7100 || SuperSPARC || || class="f_jaune | 21064 ||
|-
! 1993
| class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || ||
|-
! 1994
| <s>POWER 2</s> || PA 7200 || || || <s>21164</s> ||
|}
Vous voyez que certains processeur sont laissés de côté dans le tableau précédent. C'est car ils ne rentrent pas dans les catégories précédentes et faisaient autrement. Si on omet le Motorola 88110, qui utilisait une forme de double émission assez avancée, les deux CPUs qui vont nous intéresser sont le PA-RISC PA 7200 d'HP, et le SuperSPARC.
Le PA 7200, sorti en 1994, est un PA-7100 auquel on aurait rajouté une ALU entière. Précisons que ce processeur utilisait aussi le reconditionnement de la FPU, comme son prédécesseur. Les combinaisons autorisées étaient donc INT + INT, INT + MEM, INT + FLOAT/MUL, MEM + FLOAT/MUL. Les combinaisons autorisées par le partitionnement INT-FLOAT-MEM étaient donc possibles, en plus des combinaisons INT + INT, voire INT + INT + MUL (vu que la FPU est reconditionnée). Autrement dit, il pouvait faire toute ce que permettait un CPU à double émission entière, plus les combinaisons FLOAT/MUL + MEM.
Le SuperSPARC était un CPU triple émission, avec deux ALU entières, une FPU et une unité mémoire. Il pouvait émettre, à chaque cycle, trois µops en piochant dans deux opérations entières, un accès mémoire, une opération flottante. Le SuperSPARC avait de nombreuses contraintes d’appariement. Par exemple, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Une bonne partie de ces contraintes était lié au banc de registre, qui n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions.
: La seconde ALU a été abandonnée dans l'HyperSPARC, qui est de plus repassé en double émission, ce qui est une évolution à rebours de l'évolution des autres CPUs. Le SuperSPARC était en avance sur son temps, ce quelques années, alors que l'HyperSPARC a un design de son temps.
===Les processeurs historiques à quadruple émission===
C'est la même année , en 1994, que la quadruple émission est arrivée et que les CPU ont commencé à avoir de plus en plus d'unités de calcul. Même les designs restés en double émission avaient ajouté des unités de calcul. Il y avait alors trois stratégies pour la quadruple émission.
La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Les FPUs étaient soit dupliquées, soit scindées en deux en séparant l'additionneur du multiplieur flottant. Le POWER 2 dupliquait ses FPUs à la perfection, alors que l'21264 partitionnait la FPU. Les accès mémoire étaient traités comme des opérations entières, et le processeur pouvait : soit en faire un seul par cycle, soit en émettre deux, sous conditions (le cache devait être double port pour lire/écrire deux données à la fois, comme on le verra dans la suite du chapitre).
Les processeurs MIPS R8000 et R10000 allaient un peu plus loin, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités.
La seconde implémentation dupliquait l'ALU entière et combinait le tout avec un partitionnement INT-FLOAT-MEM. De quoi exécuter en même temps deux opérations entières, une opération flottante, et un accès mémoire. 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).
Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, avec au minimum deux ALU entières. Pour comprendre ce que je veux dire, prenons l'exemple de l'UltraSPARC. Il avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplier flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes. Le processeur pouvait émettre 4 instructions, tant qu'elles atterrissaient dans des unités différentes.
La même méthode peut être utilisée pour la double émission. Le Motorola 88110, sorti en 1991, était un CPU à double émission avec pas moins de 10 unités : deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise.
La morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes. Le tout est résumé dans le tableau ci-dessous, avec :
* en vert les CPU utilisant la première stratégie ;
* en rouge les CPU utilisant la seconde stratégie ;
* en jaune les CPU utilisant la troisième stratégie.
{|class="wikitable"
|+ Processeurs quadruple émission historiques
|-
! Année !! Processeur !! Unités
|-
! 1991
| class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques
|-
|-
! rowspan="2" | 1994
| class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'')
|-
| class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM
|-
! rowspan="3" | 1995
| class="f_jaune | PA 8000 || class="f_jaune | 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD
|-
| class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques
|-
| MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM
|-
! rowspan="2" | 1996
| class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM
|-
| class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL)
|}
===Les processeurs superscalaires modernes===
Les CPU superscalaires modernes ont des design légèrement différents des CPU historiques. Ils dupliquent l'unité entière, mais leur partitionnement est différent. En général, les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Par exemple, un CPU à 6 voies pourra émettre/exécuter 6 opérations entières simultanément, grâce à 6 ALU entières. Mais il s'agit là d'un maximum et certaines peuvent être remplacées par une opération flottante ou mémoire, qui utilisent l'unité mémoire et/ou la FPU.
Un exemple assez parlant est les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon. C'était une microarchitecture à triple émission, avec trois ALU entières et trois unités mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements.
Il s'agit là d'une tendance, cependant. Les CPU superscalaires tendent à mettre le paquet niveau opérations arithmétiques, mais pas forcément au point de remplir la totalité de la largeur du processeur. La règle est que pour N voies, le processeur dispose d'au moins N/2 ALU entières. Attention, cela ne signifie pas que le processeur émet systématiquement N/2 opération entières. Il peut par exemple émettre une combinaison d'opérations flottantes et mémoire, avec des branchements. Et N/2 est une borne basse, la plupart des CPU x86 ont environ tournent plutôt autour des 2/3, voire des 3/4.
En général, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. La conséquence est que les unités mémoire sont dupliquées, en seconde place dans l'ordre d'importance. Les processeurs x86 ont commencé à dupliquer leurs unités mémoire dès qu'ils ont atteint la quadruple émission, pour les CPU grand public. La microarchitecture Core d'Intel utilisait la quadruple émission, avec deux unités mémoire et trois ALU entières. Les microarchitectures d'AMD K5 et K6 utilisaient aussi la quadruple émission et avaient deux ALUs pour deux unités mémoire.
Plus récemment, la microarchitecture ''Golden Cove'' dispose de cinq unités de calcul trois unités mémoire (deux spécialisées dans les écritures, trois dans les lectures). La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire.Il y a donc soit égalité, soit avantage pour les ALU entières.
Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair.
==L'unité d'émission d'un processeur superscalaire==
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Voyons pourquoi.
===L'excès de ports d'émission===
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ou 5 ?
Une solution est de réduire la largeur du processeur. Dans l'exemple précédent, il est possible de passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La solution précédente est certes très intéressante, mais elle a des défauts. Le principal est que les ports d'émission sont sous-utilisés. Et les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
===L'intérieur de l'unité d'émission===
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Le banc de registre d'un processeur superscalaire===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Une autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions auto-aligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions auto-aligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez longs, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
fdai4jdg728aye61dlbb9nkz3nsypdj
773143
773142
2026-09-25T18:46:14Z
Mewtow
31375
/* La double émission entière-flottante */
773143
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===L'impact des contraintes d’appariement sur la performance===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour la suite du chapitre===
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==Le ''front-end'' des processeurs superscalaires==
Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple.
Intuitivement, charger, décoder et 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 certaines processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. 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
|}
Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs.
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
==Les processeurs implémentés avec plusieurs pipelines==
Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Vous vous dites que l'implémentation d'un processeur superscalaire n'est pas la même suivant que le CPU utilise l'exécution dans le désordre, ou non. Mais dans les faits, la distinction est plus subtile que ca. Pour l'expliquer, je vais utiliser une classification originale, qui sépare les processeurs superscalaires en trois sous-types. La distinction se fait sur la manière dont la superscalarité est implémentée en matériel.La classification que j'utilise distingue :
* Les processeurs avec plusieurs pipelines, sans exécution dans le désordre.
* Les processeurs superscalaires avec un pipeline dynamique.
* Les processeurs avec des fenêtres d'instruction décentralisées.
La distinction n'est pas qu'une question d'implémentation matérielle. Elle impacte aussi le ratio entre instructions chargées/décodées et instructions émises. Vous vous dites que les deux sont égales, mais ce n'est le cas que sur les processeurs avec N pipelines ! Sur les autres processeurs, il est possible de décoder 4 instructions, mais d'en émettre maximum 6 par cycle ! Et sur les CPU avec des fenêtres d'instruction décentralisées, il y a aussi une différence entre le nombre d'instructions qui sont ''dispatch'' et ''schedule''.
La catégorie la plus simple est celle des '''processeurs avec plusieurs pipelines'''. Ce sont des processeurs superscalaires sans exécution dans le désordre. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction.
Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, d'une FPU, éventuellement d'un second banc de registres, guère plus. Il permet d'exécuter une seconde opération de calcul, pas plus, ce qui est à l'origine de contraintes d'appariemment assez fortes. De plus, il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section.
===La double émission entière-flottante===
L'exemple le plus simple utilise un pipeline pour les opérations flottantes, séparé du pipeline originel. L'implémentation est assez simple : la FPU et le banc de registre flottant sont détachés du pipeline originel, et deviennent un pipeline séparé à eux tout seuls. Pour bien expliquer les choses, prenons un processeur avec un pipeline unique, tel qu'illustré ci-dessous. Les instructions entières et mémoire passent toutes par l'ALU entière avant d’atterrir dans l'unité mémoire, pour ensuite arriver à l'étape de ''writeback''. La FPU est un second ''back-end'', mais cela ne veut pas dire fait elle aussi une partie du pipeline, même si ce n'est pas évident.
{|
|[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]]
|[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU.]]
|}
L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
===La double émission entière===
La seconde classe de CPU superscalaires de l'époque utilisait la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Pour cela, ils peuvent dupliquer une ALU entière, ou reconditionner l'unité mémoire. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 reconditionnait l'unité mémoire en ALU entière.
L'Intel i960 utilisait le reconditionnement de l'AGU en plus du partitionnement, ce qui lui permettait d'exécuter une seconde instruction entière, en plus d'une autre instruction. Les instructions en question étaient simples : additions, quelques décalages. De nos jours, ce combo partitionnement et reconditionnement de l'AGU n'est plus utilisé. Il n'a pas beaucoup de sens quand on peut dupliquer des ALU entières, et les processeurs récents ont un budget en transistor suffisant pour.
Le Pentium dupliquait l'ALU entière, mais n'utilisaient aucune forme de partitionnement. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Contrairement au i960, le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
==Les unités de calcul d'un processeur superscalaire==
Après avoir vu le chargement et le décodage superscalaire, voyons ce qu'il en est au niveau des unités de calcul. Nous allons parler des unités de calcul avant de parler de l'émission, pour une raison simple : l'émission fait l'interface entre unité de calcul et ''front-end'' et on ne peut pas parler de l'émission sans avoir vu comment la superscalarité impacte les deux. Si le ''front-end'' charge et décode un paquet de N instructions, son exécution demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est possible d'exploiter plusieurs unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu.
Les tout premiers CPU superscalaires étaient nombreux à utiliser cette technique. Ils étaient des CPU double émission, avec une ALU entière, une unité mémoire et une FPU. Nous parlerons de '''partitionnement INT-FLOAT-MEM'''. Il s'agit de l'HyperSPARC, de l'alpha 21064 et du PowerPC 603. Les contrainte d’appariement variaient grandement suivant le processeur. Les plus permissifs autorisaient toutes les combinaisons possibles entre : une opération entière, une opération flottante, et une unité mémoire. Mais ils n'étaient pas les seuls.
Le PowerPC 603 implémentait ce genre de double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions).
Il est aussi possible de faire la même chose pour les opérations entières, vu qu'elles sont prises en charge par trois circuits. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres. Il faut parfois lui ajouter le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible d'émettre une opération séparément, ce qui donne le '''partitionnement entier''' dans ce qui suit. Le partitionnement entier n'est jamais utilisé tel quel, car le gain en performance serait trop faible au regard des couts. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Le '''reconditionnement d'une unité fonctionnelle''' permet à celle-ci de faire des opérations qu'elle n'est pas prévue pour. Par exemple, il est possible d'utiliser l'unité mémoire comme seconde ALU entière, ou d'utiliser la FPU comme multiplieur. Ces deux exemples ne sont pas choisit au hasard, nous les étudierons dans ce qui suit. Le reconditionnement évite d'avoir à dupliquer une unité de calcul, tout en ajoutant une voie. Il permet d'améliorer l'efficacité du partitionnement, qu'il soit utilisé seul ou non.
Il est possible de reconditionner la FPU pour faire des multiplications entières. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits. Il est possible d'utiliser de multiplieur pour faire des multiplications entières 32 bits. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
Le reconditionnement de la FPU permet d'exécuter une multiplication entière en plus des autres opérations entières. Par exemple, prenons un CPU avec double émission entière-flottante, sans multiplieur entier séparé. Le reconditionnement de la FPU permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine.
Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants.
Passons maintenant à une seconde forme de reconditionnement. Reconditionner l'unité mémoire permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, des versions améliorées de l'Intel 960. L'Intel 960 originel n'était pas superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire au monde. Il intégrait une ALU entière, une unité mémoire et une unité de branchement. Grâce au partitionnement, il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, en remplacement d'une µop mémoire. Son pipeline permettait donc les combinaisons INT + BRANCH + (MEM / + INT*).
Le cas de l'i960 nous montre que le reconditionnement permet d'améliorer l'intérêt du partitionnement. Elle permet de ne pas dupliquer d'unité de calcul, tout en permettant quand même d'émettre une seconde opération entière.
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''.
Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières ! Il doit y avoir deux ALU entières pour de la double émission entière, trois pour de triple émission éntière, 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]]
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications.
Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que dupliquer l'ALU entière peut se marier parfaitement avec la présence d'une FPU. Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU reconditionne sa FPU ou pas. Mais il est très rare qu'un CPU ait à la fois un multiplieur entier et une FPU reconditionnée.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires modernes utilisent toutes ces stratégies===
Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, duplication de l'ALU entière, duplication des autre unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents.
* Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU.
* Le '''reconditionnement des unités''' a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU.
* La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants.
* La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables.
De nos jours, le partitionnement est utilisé en complément de la duplication des ALU entières, et de la duplication des autres unités. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993.
Le meilleur moyen de voir comment sont combinés ces quatre technique est de regarder ce qu'il s'est passé historiquement.
==L'évolution historique de la superscalarité==
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. La plupart des CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 sont rapidement montés à de la quadruple émission, avec quelques exceptions. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 ||
|-
! 1995
| 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000
|-
! 1996
| class="f_bleu | 620 || || || || class="f_bleu | 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
===Les processeurs historiques à double émission===
Pour commencer, étudions le cas des premiers CPU superscalaires à double émission. Ils sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Notamment, la duplication des autres unités ne sert pour ainsi dire à rien, ce qui limite les possibilités, mais rendra les explications plus simples.
Il pouvaient se classer en deux/trois types principaux, assez stricts, avec quelques processeurs qui ne rentraient pas dans ces cases. Le premier utilise le partitionnement seul, le second dupliquer les ALU entières sans partitionnement, et le troisième type est volontairement passé sous silence pour le moment (nous aborderons le troisième type dans la partie sur des design à quadruple émission).
Les CPU n'utilisant que le partitionnement faisaient la double émission avec trois unités : une ALU, une FPU et une unité mémoire. Le reconditionnement de la FPU était parfois utilisé, mais l'AGU n'était jamais reconditionnée. Si la moitié des processeurs faisait du ''partitionnement complet'' entre ALU, FPU et unité mémoire, l'autre moitié utilisait la bouble émission entière-flottante.
Pour résumer, voici le tableau chronologique disant quel CPU utilisait quelle technique.
* Le partitionnement INT-FLOAT-MEM est en jaune.
* La double émission entière-flottante est en vert.
* La double émission entière est en rouge.
* Les CPU barrés utilisent la quadruple émission et seront vu dans la section suivante.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| class="f_vert | RS/6000 || || || class="f_rouge | i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| class="f_vert | RSC || class="f_vert | PA 7100 || SuperSPARC || || class="f_jaune | 21064 ||
|-
! 1993
| class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || ||
|-
! 1994
| <s>POWER 2</s> || PA 7200 || || || <s>21164</s> ||
|}
Vous voyez que certains processeur sont laissés de côté dans le tableau précédent. C'est car ils ne rentrent pas dans les catégories précédentes et faisaient autrement. Si on omet le Motorola 88110, qui utilisait une forme de double émission assez avancée, les deux CPUs qui vont nous intéresser sont le PA-RISC PA 7200 d'HP, et le SuperSPARC.
Le PA 7200, sorti en 1994, est un PA-7100 auquel on aurait rajouté une ALU entière. Précisons que ce processeur utilisait aussi le reconditionnement de la FPU, comme son prédécesseur. Les combinaisons autorisées étaient donc INT + INT, INT + MEM, INT + FLOAT/MUL, MEM + FLOAT/MUL. Les combinaisons autorisées par le partitionnement INT-FLOAT-MEM étaient donc possibles, en plus des combinaisons INT + INT, voire INT + INT + MUL (vu que la FPU est reconditionnée). Autrement dit, il pouvait faire toute ce que permettait un CPU à double émission entière, plus les combinaisons FLOAT/MUL + MEM.
Le SuperSPARC était un CPU triple émission, avec deux ALU entières, une FPU et une unité mémoire. Il pouvait émettre, à chaque cycle, trois µops en piochant dans deux opérations entières, un accès mémoire, une opération flottante. Le SuperSPARC avait de nombreuses contraintes d’appariement. Par exemple, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Une bonne partie de ces contraintes était lié au banc de registre, qui n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions.
: La seconde ALU a été abandonnée dans l'HyperSPARC, qui est de plus repassé en double émission, ce qui est une évolution à rebours de l'évolution des autres CPUs. Le SuperSPARC était en avance sur son temps, ce quelques années, alors que l'HyperSPARC a un design de son temps.
===Les processeurs historiques à quadruple émission===
C'est la même année , en 1994, que la quadruple émission est arrivée et que les CPU ont commencé à avoir de plus en plus d'unités de calcul. Même les designs restés en double émission avaient ajouté des unités de calcul. Il y avait alors trois stratégies pour la quadruple émission.
La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Les FPUs étaient soit dupliquées, soit scindées en deux en séparant l'additionneur du multiplieur flottant. Le POWER 2 dupliquait ses FPUs à la perfection, alors que l'21264 partitionnait la FPU. Les accès mémoire étaient traités comme des opérations entières, et le processeur pouvait : soit en faire un seul par cycle, soit en émettre deux, sous conditions (le cache devait être double port pour lire/écrire deux données à la fois, comme on le verra dans la suite du chapitre).
Les processeurs MIPS R8000 et R10000 allaient un peu plus loin, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités.
La seconde implémentation dupliquait l'ALU entière et combinait le tout avec un partitionnement INT-FLOAT-MEM. De quoi exécuter en même temps deux opérations entières, une opération flottante, et un accès mémoire. 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).
Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, avec au minimum deux ALU entières. Pour comprendre ce que je veux dire, prenons l'exemple de l'UltraSPARC. Il avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplier flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes. Le processeur pouvait émettre 4 instructions, tant qu'elles atterrissaient dans des unités différentes.
La même méthode peut être utilisée pour la double émission. Le Motorola 88110, sorti en 1991, était un CPU à double émission avec pas moins de 10 unités : deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise.
La morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes. Le tout est résumé dans le tableau ci-dessous, avec :
* en vert les CPU utilisant la première stratégie ;
* en rouge les CPU utilisant la seconde stratégie ;
* en jaune les CPU utilisant la troisième stratégie.
{|class="wikitable"
|+ Processeurs quadruple émission historiques
|-
! Année !! Processeur !! Unités
|-
! 1991
| class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques
|-
|-
! rowspan="2" | 1994
| class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'')
|-
| class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM
|-
! rowspan="3" | 1995
| class="f_jaune | PA 8000 || class="f_jaune | 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD
|-
| class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques
|-
| MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM
|-
! rowspan="2" | 1996
| class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM
|-
| class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL)
|}
===Les processeurs superscalaires modernes===
Les CPU superscalaires modernes ont des design légèrement différents des CPU historiques. Ils dupliquent l'unité entière, mais leur partitionnement est différent. En général, les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Par exemple, un CPU à 6 voies pourra émettre/exécuter 6 opérations entières simultanément, grâce à 6 ALU entières. Mais il s'agit là d'un maximum et certaines peuvent être remplacées par une opération flottante ou mémoire, qui utilisent l'unité mémoire et/ou la FPU.
Un exemple assez parlant est les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon. C'était une microarchitecture à triple émission, avec trois ALU entières et trois unités mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements.
Il s'agit là d'une tendance, cependant. Les CPU superscalaires tendent à mettre le paquet niveau opérations arithmétiques, mais pas forcément au point de remplir la totalité de la largeur du processeur. La règle est que pour N voies, le processeur dispose d'au moins N/2 ALU entières. Attention, cela ne signifie pas que le processeur émet systématiquement N/2 opération entières. Il peut par exemple émettre une combinaison d'opérations flottantes et mémoire, avec des branchements. Et N/2 est une borne basse, la plupart des CPU x86 ont environ tournent plutôt autour des 2/3, voire des 3/4.
En général, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. La conséquence est que les unités mémoire sont dupliquées, en seconde place dans l'ordre d'importance. Les processeurs x86 ont commencé à dupliquer leurs unités mémoire dès qu'ils ont atteint la quadruple émission, pour les CPU grand public. La microarchitecture Core d'Intel utilisait la quadruple émission, avec deux unités mémoire et trois ALU entières. Les microarchitectures d'AMD K5 et K6 utilisaient aussi la quadruple émission et avaient deux ALUs pour deux unités mémoire.
Plus récemment, la microarchitecture ''Golden Cove'' dispose de cinq unités de calcul trois unités mémoire (deux spécialisées dans les écritures, trois dans les lectures). La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire.Il y a donc soit égalité, soit avantage pour les ALU entières.
Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair.
==L'unité d'émission d'un processeur superscalaire==
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Voyons pourquoi.
===L'excès de ports d'émission===
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ou 5 ?
Une solution est de réduire la largeur du processeur. Dans l'exemple précédent, il est possible de passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La solution précédente est certes très intéressante, mais elle a des défauts. Le principal est que les ports d'émission sont sous-utilisés. Et les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
===L'intérieur de l'unité d'émission===
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Le banc de registre d'un processeur superscalaire===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Une autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions auto-aligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions auto-aligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez longs, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
p4vxbg60cxt6i6514ud6do36195j5kd
773144
773143
2026-09-25T18:51:10Z
Mewtow
31375
/* La double émission entière-flottante */
773144
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===L'impact des contraintes d’appariement sur la performance===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour la suite du chapitre===
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==Le ''front-end'' des processeurs superscalaires==
Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple.
Intuitivement, charger, décoder et 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 certaines processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. 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
|}
Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs.
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
==Les processeurs implémentés avec plusieurs pipelines==
Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Vous vous dites que l'implémentation d'un processeur superscalaire n'est pas la même suivant que le CPU utilise l'exécution dans le désordre, ou non. Mais dans les faits, la distinction est plus subtile que ca. Pour l'expliquer, je vais utiliser une classification originale, qui sépare les processeurs superscalaires en trois sous-types. La distinction se fait sur la manière dont la superscalarité est implémentée en matériel.La classification que j'utilise distingue :
* Les processeurs avec plusieurs pipelines, sans exécution dans le désordre.
* Les processeurs superscalaires avec un pipeline dynamique.
* Les processeurs avec des fenêtres d'instruction décentralisées.
La distinction n'est pas qu'une question d'implémentation matérielle. Elle impacte aussi le ratio entre instructions chargées/décodées et instructions émises. Vous vous dites que les deux sont égales, mais ce n'est le cas que sur les processeurs avec N pipelines ! Sur les autres processeurs, il est possible de décoder 4 instructions, mais d'en émettre maximum 6 par cycle ! Et sur les CPU avec des fenêtres d'instruction décentralisées, il y a aussi une différence entre le nombre d'instructions qui sont ''dispatch'' et ''schedule''.
La catégorie la plus simple est celle des '''processeurs avec plusieurs pipelines'''. Ce sont des processeurs superscalaires sans exécution dans le désordre. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction.
Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, d'une FPU, éventuellement d'un second banc de registres, guère plus. Il permet d'exécuter une seconde opération de calcul, pas plus, ce qui est à l'origine de contraintes d'appariemment assez fortes. De plus, il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section.
===La double émission entière-flottante===
L'exemple le plus simple utilise un pipeline pour les opérations flottantes, séparé du pipeline originel. L'implémentation est assez simple : la FPU et le banc de registre flottant sont détachés du pipeline originel, et deviennent un pipeline séparé à eux tout seuls.
L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Pour bien expliquer les choses, prenons le processeur illustré ci-dessous. Un processeur a deux ''back-end'' séparés, un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par l'ALU entière, passent éventuellement dans l'unité mémoire, pour ensuite arriver à l'étape de ''writeback''. La FPU et le banc de registre flottant ont eux aussi leur propre ''back-end'' séparé. Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaqaue pipeline, à chaque cycle.
[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]]
Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
===La double émission entière===
La seconde classe de CPU superscalaires de l'époque utilisait la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Pour cela, ils peuvent dupliquer une ALU entière, ou reconditionner l'unité mémoire. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 reconditionnait l'unité mémoire en ALU entière.
L'Intel i960 utilisait le reconditionnement de l'AGU en plus du partitionnement, ce qui lui permettait d'exécuter une seconde instruction entière, en plus d'une autre instruction. Les instructions en question étaient simples : additions, quelques décalages. De nos jours, ce combo partitionnement et reconditionnement de l'AGU n'est plus utilisé. Il n'a pas beaucoup de sens quand on peut dupliquer des ALU entières, et les processeurs récents ont un budget en transistor suffisant pour.
Le Pentium dupliquait l'ALU entière, mais n'utilisaient aucune forme de partitionnement. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Contrairement au i960, le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
==Les unités de calcul d'un processeur superscalaire==
Après avoir vu le chargement et le décodage superscalaire, voyons ce qu'il en est au niveau des unités de calcul. Nous allons parler des unités de calcul avant de parler de l'émission, pour une raison simple : l'émission fait l'interface entre unité de calcul et ''front-end'' et on ne peut pas parler de l'émission sans avoir vu comment la superscalarité impacte les deux. Si le ''front-end'' charge et décode un paquet de N instructions, son exécution demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est possible d'exploiter plusieurs unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu.
Les tout premiers CPU superscalaires étaient nombreux à utiliser cette technique. Ils étaient des CPU double émission, avec une ALU entière, une unité mémoire et une FPU. Nous parlerons de '''partitionnement INT-FLOAT-MEM'''. Il s'agit de l'HyperSPARC, de l'alpha 21064 et du PowerPC 603. Les contrainte d’appariement variaient grandement suivant le processeur. Les plus permissifs autorisaient toutes les combinaisons possibles entre : une opération entière, une opération flottante, et une unité mémoire. Mais ils n'étaient pas les seuls.
Le PowerPC 603 implémentait ce genre de double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions).
Il est aussi possible de faire la même chose pour les opérations entières, vu qu'elles sont prises en charge par trois circuits. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres. Il faut parfois lui ajouter le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible d'émettre une opération séparément, ce qui donne le '''partitionnement entier''' dans ce qui suit. Le partitionnement entier n'est jamais utilisé tel quel, car le gain en performance serait trop faible au regard des couts. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Le '''reconditionnement d'une unité fonctionnelle''' permet à celle-ci de faire des opérations qu'elle n'est pas prévue pour. Par exemple, il est possible d'utiliser l'unité mémoire comme seconde ALU entière, ou d'utiliser la FPU comme multiplieur. Ces deux exemples ne sont pas choisit au hasard, nous les étudierons dans ce qui suit. Le reconditionnement évite d'avoir à dupliquer une unité de calcul, tout en ajoutant une voie. Il permet d'améliorer l'efficacité du partitionnement, qu'il soit utilisé seul ou non.
Il est possible de reconditionner la FPU pour faire des multiplications entières. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits. Il est possible d'utiliser de multiplieur pour faire des multiplications entières 32 bits. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
Le reconditionnement de la FPU permet d'exécuter une multiplication entière en plus des autres opérations entières. Par exemple, prenons un CPU avec double émission entière-flottante, sans multiplieur entier séparé. Le reconditionnement de la FPU permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine.
Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants.
Passons maintenant à une seconde forme de reconditionnement. Reconditionner l'unité mémoire permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, des versions améliorées de l'Intel 960. L'Intel 960 originel n'était pas superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire au monde. Il intégrait une ALU entière, une unité mémoire et une unité de branchement. Grâce au partitionnement, il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, en remplacement d'une µop mémoire. Son pipeline permettait donc les combinaisons INT + BRANCH + (MEM / + INT*).
Le cas de l'i960 nous montre que le reconditionnement permet d'améliorer l'intérêt du partitionnement. Elle permet de ne pas dupliquer d'unité de calcul, tout en permettant quand même d'émettre une seconde opération entière.
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''.
Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières ! Il doit y avoir deux ALU entières pour de la double émission entière, trois pour de triple émission éntière, 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]]
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications.
Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que dupliquer l'ALU entière peut se marier parfaitement avec la présence d'une FPU. Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU reconditionne sa FPU ou pas. Mais il est très rare qu'un CPU ait à la fois un multiplieur entier et une FPU reconditionnée.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires modernes utilisent toutes ces stratégies===
Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, duplication de l'ALU entière, duplication des autre unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents.
* Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU.
* Le '''reconditionnement des unités''' a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU.
* La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants.
* La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables.
De nos jours, le partitionnement est utilisé en complément de la duplication des ALU entières, et de la duplication des autres unités. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993.
Le meilleur moyen de voir comment sont combinés ces quatre technique est de regarder ce qu'il s'est passé historiquement.
==L'évolution historique de la superscalarité==
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. La plupart des CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 sont rapidement montés à de la quadruple émission, avec quelques exceptions. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 ||
|-
! 1995
| 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000
|-
! 1996
| class="f_bleu | 620 || || || || class="f_bleu | 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
===Les processeurs historiques à double émission===
Pour commencer, étudions le cas des premiers CPU superscalaires à double émission. Ils sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Notamment, la duplication des autres unités ne sert pour ainsi dire à rien, ce qui limite les possibilités, mais rendra les explications plus simples.
Il pouvaient se classer en deux/trois types principaux, assez stricts, avec quelques processeurs qui ne rentraient pas dans ces cases. Le premier utilise le partitionnement seul, le second dupliquer les ALU entières sans partitionnement, et le troisième type est volontairement passé sous silence pour le moment (nous aborderons le troisième type dans la partie sur des design à quadruple émission).
Les CPU n'utilisant que le partitionnement faisaient la double émission avec trois unités : une ALU, une FPU et une unité mémoire. Le reconditionnement de la FPU était parfois utilisé, mais l'AGU n'était jamais reconditionnée. Si la moitié des processeurs faisait du ''partitionnement complet'' entre ALU, FPU et unité mémoire, l'autre moitié utilisait la bouble émission entière-flottante.
Pour résumer, voici le tableau chronologique disant quel CPU utilisait quelle technique.
* Le partitionnement INT-FLOAT-MEM est en jaune.
* La double émission entière-flottante est en vert.
* La double émission entière est en rouge.
* Les CPU barrés utilisent la quadruple émission et seront vu dans la section suivante.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| class="f_vert | RS/6000 || || || class="f_rouge | i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| class="f_vert | RSC || class="f_vert | PA 7100 || SuperSPARC || || class="f_jaune | 21064 ||
|-
! 1993
| class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || ||
|-
! 1994
| <s>POWER 2</s> || PA 7200 || || || <s>21164</s> ||
|}
Vous voyez que certains processeur sont laissés de côté dans le tableau précédent. C'est car ils ne rentrent pas dans les catégories précédentes et faisaient autrement. Si on omet le Motorola 88110, qui utilisait une forme de double émission assez avancée, les deux CPUs qui vont nous intéresser sont le PA-RISC PA 7200 d'HP, et le SuperSPARC.
Le PA 7200, sorti en 1994, est un PA-7100 auquel on aurait rajouté une ALU entière. Précisons que ce processeur utilisait aussi le reconditionnement de la FPU, comme son prédécesseur. Les combinaisons autorisées étaient donc INT + INT, INT + MEM, INT + FLOAT/MUL, MEM + FLOAT/MUL. Les combinaisons autorisées par le partitionnement INT-FLOAT-MEM étaient donc possibles, en plus des combinaisons INT + INT, voire INT + INT + MUL (vu que la FPU est reconditionnée). Autrement dit, il pouvait faire toute ce que permettait un CPU à double émission entière, plus les combinaisons FLOAT/MUL + MEM.
Le SuperSPARC était un CPU triple émission, avec deux ALU entières, une FPU et une unité mémoire. Il pouvait émettre, à chaque cycle, trois µops en piochant dans deux opérations entières, un accès mémoire, une opération flottante. Le SuperSPARC avait de nombreuses contraintes d’appariement. Par exemple, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Une bonne partie de ces contraintes était lié au banc de registre, qui n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions.
: La seconde ALU a été abandonnée dans l'HyperSPARC, qui est de plus repassé en double émission, ce qui est une évolution à rebours de l'évolution des autres CPUs. Le SuperSPARC était en avance sur son temps, ce quelques années, alors que l'HyperSPARC a un design de son temps.
===Les processeurs historiques à quadruple émission===
C'est la même année , en 1994, que la quadruple émission est arrivée et que les CPU ont commencé à avoir de plus en plus d'unités de calcul. Même les designs restés en double émission avaient ajouté des unités de calcul. Il y avait alors trois stratégies pour la quadruple émission.
La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Les FPUs étaient soit dupliquées, soit scindées en deux en séparant l'additionneur du multiplieur flottant. Le POWER 2 dupliquait ses FPUs à la perfection, alors que l'21264 partitionnait la FPU. Les accès mémoire étaient traités comme des opérations entières, et le processeur pouvait : soit en faire un seul par cycle, soit en émettre deux, sous conditions (le cache devait être double port pour lire/écrire deux données à la fois, comme on le verra dans la suite du chapitre).
Les processeurs MIPS R8000 et R10000 allaient un peu plus loin, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités.
La seconde implémentation dupliquait l'ALU entière et combinait le tout avec un partitionnement INT-FLOAT-MEM. De quoi exécuter en même temps deux opérations entières, une opération flottante, et un accès mémoire. 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).
Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, avec au minimum deux ALU entières. Pour comprendre ce que je veux dire, prenons l'exemple de l'UltraSPARC. Il avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplier flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes. Le processeur pouvait émettre 4 instructions, tant qu'elles atterrissaient dans des unités différentes.
La même méthode peut être utilisée pour la double émission. Le Motorola 88110, sorti en 1991, était un CPU à double émission avec pas moins de 10 unités : deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise.
La morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes. Le tout est résumé dans le tableau ci-dessous, avec :
* en vert les CPU utilisant la première stratégie ;
* en rouge les CPU utilisant la seconde stratégie ;
* en jaune les CPU utilisant la troisième stratégie.
{|class="wikitable"
|+ Processeurs quadruple émission historiques
|-
! Année !! Processeur !! Unités
|-
! 1991
| class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques
|-
|-
! rowspan="2" | 1994
| class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'')
|-
| class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM
|-
! rowspan="3" | 1995
| class="f_jaune | PA 8000 || class="f_jaune | 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD
|-
| class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques
|-
| MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM
|-
! rowspan="2" | 1996
| class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM
|-
| class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL)
|}
===Les processeurs superscalaires modernes===
Les CPU superscalaires modernes ont des design légèrement différents des CPU historiques. Ils dupliquent l'unité entière, mais leur partitionnement est différent. En général, les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Par exemple, un CPU à 6 voies pourra émettre/exécuter 6 opérations entières simultanément, grâce à 6 ALU entières. Mais il s'agit là d'un maximum et certaines peuvent être remplacées par une opération flottante ou mémoire, qui utilisent l'unité mémoire et/ou la FPU.
Un exemple assez parlant est les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon. C'était une microarchitecture à triple émission, avec trois ALU entières et trois unités mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements.
Il s'agit là d'une tendance, cependant. Les CPU superscalaires tendent à mettre le paquet niveau opérations arithmétiques, mais pas forcément au point de remplir la totalité de la largeur du processeur. La règle est que pour N voies, le processeur dispose d'au moins N/2 ALU entières. Attention, cela ne signifie pas que le processeur émet systématiquement N/2 opération entières. Il peut par exemple émettre une combinaison d'opérations flottantes et mémoire, avec des branchements. Et N/2 est une borne basse, la plupart des CPU x86 ont environ tournent plutôt autour des 2/3, voire des 3/4.
En général, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. La conséquence est que les unités mémoire sont dupliquées, en seconde place dans l'ordre d'importance. Les processeurs x86 ont commencé à dupliquer leurs unités mémoire dès qu'ils ont atteint la quadruple émission, pour les CPU grand public. La microarchitecture Core d'Intel utilisait la quadruple émission, avec deux unités mémoire et trois ALU entières. Les microarchitectures d'AMD K5 et K6 utilisaient aussi la quadruple émission et avaient deux ALUs pour deux unités mémoire.
Plus récemment, la microarchitecture ''Golden Cove'' dispose de cinq unités de calcul trois unités mémoire (deux spécialisées dans les écritures, trois dans les lectures). La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire.Il y a donc soit égalité, soit avantage pour les ALU entières.
Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair.
==L'unité d'émission d'un processeur superscalaire==
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Voyons pourquoi.
===L'excès de ports d'émission===
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ou 5 ?
Une solution est de réduire la largeur du processeur. Dans l'exemple précédent, il est possible de passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La solution précédente est certes très intéressante, mais elle a des défauts. Le principal est que les ports d'émission sont sous-utilisés. Et les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
===L'intérieur de l'unité d'émission===
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Le banc de registre d'un processeur superscalaire===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Une autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions auto-aligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions auto-aligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez longs, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
4kxtvt3i3j7nj7shinzzxwsxu07flt7
773145
773144
2026-09-25T18:51:33Z
Mewtow
31375
/* La double émission entière-flottante */
773145
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===L'impact des contraintes d’appariement sur la performance===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour la suite du chapitre===
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==Le ''front-end'' des processeurs superscalaires==
Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple.
Intuitivement, charger, décoder et 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 certaines processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. 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
|}
Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs.
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
==Les processeurs implémentés avec plusieurs pipelines==
Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Vous vous dites que l'implémentation d'un processeur superscalaire n'est pas la même suivant que le CPU utilise l'exécution dans le désordre, ou non. Mais dans les faits, la distinction est plus subtile que ca. Pour l'expliquer, je vais utiliser une classification originale, qui sépare les processeurs superscalaires en trois sous-types. La distinction se fait sur la manière dont la superscalarité est implémentée en matériel.La classification que j'utilise distingue :
* Les processeurs avec plusieurs pipelines, sans exécution dans le désordre.
* Les processeurs superscalaires avec un pipeline dynamique.
* Les processeurs avec des fenêtres d'instruction décentralisées.
La distinction n'est pas qu'une question d'implémentation matérielle. Elle impacte aussi le ratio entre instructions chargées/décodées et instructions émises. Vous vous dites que les deux sont égales, mais ce n'est le cas que sur les processeurs avec N pipelines ! Sur les autres processeurs, il est possible de décoder 4 instructions, mais d'en émettre maximum 6 par cycle ! Et sur les CPU avec des fenêtres d'instruction décentralisées, il y a aussi une différence entre le nombre d'instructions qui sont ''dispatch'' et ''schedule''.
La catégorie la plus simple est celle des '''processeurs avec plusieurs pipelines'''. Ce sont des processeurs superscalaires sans exécution dans le désordre. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction.
Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, d'une FPU, éventuellement d'un second banc de registres, guère plus. Il permet d'exécuter une seconde opération de calcul, pas plus, ce qui est à l'origine de contraintes d'appariemment assez fortes. De plus, il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section.
===La double émission entière-flottante===
L'exemple le plus simple utilise un pipeline pour les opérations flottantes, séparé du pipeline originel. Pour bien expliquer les choses, prenons le processeur illustré ci-dessous. Un processeur a deux ''back-end'' séparés, un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par l'ALU entière, passent éventuellement dans l'unité mémoire, pour ensuite arriver à l'étape de ''writeback''. La FPU et le banc de registre flottant ont eux aussi leur propre ''back-end'' séparé. Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaqaue pipeline, à chaque cycle.
[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]]
L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
===La double émission entière===
La seconde classe de CPU superscalaires de l'époque utilisait la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Pour cela, ils peuvent dupliquer une ALU entière, ou reconditionner l'unité mémoire. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 reconditionnait l'unité mémoire en ALU entière.
L'Intel i960 utilisait le reconditionnement de l'AGU en plus du partitionnement, ce qui lui permettait d'exécuter une seconde instruction entière, en plus d'une autre instruction. Les instructions en question étaient simples : additions, quelques décalages. De nos jours, ce combo partitionnement et reconditionnement de l'AGU n'est plus utilisé. Il n'a pas beaucoup de sens quand on peut dupliquer des ALU entières, et les processeurs récents ont un budget en transistor suffisant pour.
Le Pentium dupliquait l'ALU entière, mais n'utilisaient aucune forme de partitionnement. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Contrairement au i960, le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
==Les unités de calcul d'un processeur superscalaire==
Après avoir vu le chargement et le décodage superscalaire, voyons ce qu'il en est au niveau des unités de calcul. Nous allons parler des unités de calcul avant de parler de l'émission, pour une raison simple : l'émission fait l'interface entre unité de calcul et ''front-end'' et on ne peut pas parler de l'émission sans avoir vu comment la superscalarité impacte les deux. Si le ''front-end'' charge et décode un paquet de N instructions, son exécution demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est possible d'exploiter plusieurs unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu.
Les tout premiers CPU superscalaires étaient nombreux à utiliser cette technique. Ils étaient des CPU double émission, avec une ALU entière, une unité mémoire et une FPU. Nous parlerons de '''partitionnement INT-FLOAT-MEM'''. Il s'agit de l'HyperSPARC, de l'alpha 21064 et du PowerPC 603. Les contrainte d’appariement variaient grandement suivant le processeur. Les plus permissifs autorisaient toutes les combinaisons possibles entre : une opération entière, une opération flottante, et une unité mémoire. Mais ils n'étaient pas les seuls.
Le PowerPC 603 implémentait ce genre de double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions).
Il est aussi possible de faire la même chose pour les opérations entières, vu qu'elles sont prises en charge par trois circuits. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres. Il faut parfois lui ajouter le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible d'émettre une opération séparément, ce qui donne le '''partitionnement entier''' dans ce qui suit. Le partitionnement entier n'est jamais utilisé tel quel, car le gain en performance serait trop faible au regard des couts. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Le '''reconditionnement d'une unité fonctionnelle''' permet à celle-ci de faire des opérations qu'elle n'est pas prévue pour. Par exemple, il est possible d'utiliser l'unité mémoire comme seconde ALU entière, ou d'utiliser la FPU comme multiplieur. Ces deux exemples ne sont pas choisit au hasard, nous les étudierons dans ce qui suit. Le reconditionnement évite d'avoir à dupliquer une unité de calcul, tout en ajoutant une voie. Il permet d'améliorer l'efficacité du partitionnement, qu'il soit utilisé seul ou non.
Il est possible de reconditionner la FPU pour faire des multiplications entières. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits. Il est possible d'utiliser de multiplieur pour faire des multiplications entières 32 bits. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
Le reconditionnement de la FPU permet d'exécuter une multiplication entière en plus des autres opérations entières. Par exemple, prenons un CPU avec double émission entière-flottante, sans multiplieur entier séparé. Le reconditionnement de la FPU permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine.
Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants.
Passons maintenant à une seconde forme de reconditionnement. Reconditionner l'unité mémoire permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, des versions améliorées de l'Intel 960. L'Intel 960 originel n'était pas superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire au monde. Il intégrait une ALU entière, une unité mémoire et une unité de branchement. Grâce au partitionnement, il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, en remplacement d'une µop mémoire. Son pipeline permettait donc les combinaisons INT + BRANCH + (MEM / + INT*).
Le cas de l'i960 nous montre que le reconditionnement permet d'améliorer l'intérêt du partitionnement. Elle permet de ne pas dupliquer d'unité de calcul, tout en permettant quand même d'émettre une seconde opération entière.
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''.
Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières ! Il doit y avoir deux ALU entières pour de la double émission entière, trois pour de triple émission éntière, 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]]
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications.
Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que dupliquer l'ALU entière peut se marier parfaitement avec la présence d'une FPU. Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU reconditionne sa FPU ou pas. Mais il est très rare qu'un CPU ait à la fois un multiplieur entier et une FPU reconditionnée.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires modernes utilisent toutes ces stratégies===
Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, duplication de l'ALU entière, duplication des autre unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents.
* Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU.
* Le '''reconditionnement des unités''' a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU.
* La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants.
* La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables.
De nos jours, le partitionnement est utilisé en complément de la duplication des ALU entières, et de la duplication des autres unités. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993.
Le meilleur moyen de voir comment sont combinés ces quatre technique est de regarder ce qu'il s'est passé historiquement.
==L'évolution historique de la superscalarité==
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. La plupart des CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 sont rapidement montés à de la quadruple émission, avec quelques exceptions. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 ||
|-
! 1995
| 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000
|-
! 1996
| class="f_bleu | 620 || || || || class="f_bleu | 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
===Les processeurs historiques à double émission===
Pour commencer, étudions le cas des premiers CPU superscalaires à double émission. Ils sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Notamment, la duplication des autres unités ne sert pour ainsi dire à rien, ce qui limite les possibilités, mais rendra les explications plus simples.
Il pouvaient se classer en deux/trois types principaux, assez stricts, avec quelques processeurs qui ne rentraient pas dans ces cases. Le premier utilise le partitionnement seul, le second dupliquer les ALU entières sans partitionnement, et le troisième type est volontairement passé sous silence pour le moment (nous aborderons le troisième type dans la partie sur des design à quadruple émission).
Les CPU n'utilisant que le partitionnement faisaient la double émission avec trois unités : une ALU, une FPU et une unité mémoire. Le reconditionnement de la FPU était parfois utilisé, mais l'AGU n'était jamais reconditionnée. Si la moitié des processeurs faisait du ''partitionnement complet'' entre ALU, FPU et unité mémoire, l'autre moitié utilisait la bouble émission entière-flottante.
Pour résumer, voici le tableau chronologique disant quel CPU utilisait quelle technique.
* Le partitionnement INT-FLOAT-MEM est en jaune.
* La double émission entière-flottante est en vert.
* La double émission entière est en rouge.
* Les CPU barrés utilisent la quadruple émission et seront vu dans la section suivante.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| class="f_vert | RS/6000 || || || class="f_rouge | i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| class="f_vert | RSC || class="f_vert | PA 7100 || SuperSPARC || || class="f_jaune | 21064 ||
|-
! 1993
| class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || ||
|-
! 1994
| <s>POWER 2</s> || PA 7200 || || || <s>21164</s> ||
|}
Vous voyez que certains processeur sont laissés de côté dans le tableau précédent. C'est car ils ne rentrent pas dans les catégories précédentes et faisaient autrement. Si on omet le Motorola 88110, qui utilisait une forme de double émission assez avancée, les deux CPUs qui vont nous intéresser sont le PA-RISC PA 7200 d'HP, et le SuperSPARC.
Le PA 7200, sorti en 1994, est un PA-7100 auquel on aurait rajouté une ALU entière. Précisons que ce processeur utilisait aussi le reconditionnement de la FPU, comme son prédécesseur. Les combinaisons autorisées étaient donc INT + INT, INT + MEM, INT + FLOAT/MUL, MEM + FLOAT/MUL. Les combinaisons autorisées par le partitionnement INT-FLOAT-MEM étaient donc possibles, en plus des combinaisons INT + INT, voire INT + INT + MUL (vu que la FPU est reconditionnée). Autrement dit, il pouvait faire toute ce que permettait un CPU à double émission entière, plus les combinaisons FLOAT/MUL + MEM.
Le SuperSPARC était un CPU triple émission, avec deux ALU entières, une FPU et une unité mémoire. Il pouvait émettre, à chaque cycle, trois µops en piochant dans deux opérations entières, un accès mémoire, une opération flottante. Le SuperSPARC avait de nombreuses contraintes d’appariement. Par exemple, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Une bonne partie de ces contraintes était lié au banc de registre, qui n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions.
: La seconde ALU a été abandonnée dans l'HyperSPARC, qui est de plus repassé en double émission, ce qui est une évolution à rebours de l'évolution des autres CPUs. Le SuperSPARC était en avance sur son temps, ce quelques années, alors que l'HyperSPARC a un design de son temps.
===Les processeurs historiques à quadruple émission===
C'est la même année , en 1994, que la quadruple émission est arrivée et que les CPU ont commencé à avoir de plus en plus d'unités de calcul. Même les designs restés en double émission avaient ajouté des unités de calcul. Il y avait alors trois stratégies pour la quadruple émission.
La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Les FPUs étaient soit dupliquées, soit scindées en deux en séparant l'additionneur du multiplieur flottant. Le POWER 2 dupliquait ses FPUs à la perfection, alors que l'21264 partitionnait la FPU. Les accès mémoire étaient traités comme des opérations entières, et le processeur pouvait : soit en faire un seul par cycle, soit en émettre deux, sous conditions (le cache devait être double port pour lire/écrire deux données à la fois, comme on le verra dans la suite du chapitre).
Les processeurs MIPS R8000 et R10000 allaient un peu plus loin, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités.
La seconde implémentation dupliquait l'ALU entière et combinait le tout avec un partitionnement INT-FLOAT-MEM. De quoi exécuter en même temps deux opérations entières, une opération flottante, et un accès mémoire. 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).
Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, avec au minimum deux ALU entières. Pour comprendre ce que je veux dire, prenons l'exemple de l'UltraSPARC. Il avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplier flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes. Le processeur pouvait émettre 4 instructions, tant qu'elles atterrissaient dans des unités différentes.
La même méthode peut être utilisée pour la double émission. Le Motorola 88110, sorti en 1991, était un CPU à double émission avec pas moins de 10 unités : deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise.
La morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes. Le tout est résumé dans le tableau ci-dessous, avec :
* en vert les CPU utilisant la première stratégie ;
* en rouge les CPU utilisant la seconde stratégie ;
* en jaune les CPU utilisant la troisième stratégie.
{|class="wikitable"
|+ Processeurs quadruple émission historiques
|-
! Année !! Processeur !! Unités
|-
! 1991
| class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques
|-
|-
! rowspan="2" | 1994
| class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'')
|-
| class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM
|-
! rowspan="3" | 1995
| class="f_jaune | PA 8000 || class="f_jaune | 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD
|-
| class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques
|-
| MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM
|-
! rowspan="2" | 1996
| class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM
|-
| class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL)
|}
===Les processeurs superscalaires modernes===
Les CPU superscalaires modernes ont des design légèrement différents des CPU historiques. Ils dupliquent l'unité entière, mais leur partitionnement est différent. En général, les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Par exemple, un CPU à 6 voies pourra émettre/exécuter 6 opérations entières simultanément, grâce à 6 ALU entières. Mais il s'agit là d'un maximum et certaines peuvent être remplacées par une opération flottante ou mémoire, qui utilisent l'unité mémoire et/ou la FPU.
Un exemple assez parlant est les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon. C'était une microarchitecture à triple émission, avec trois ALU entières et trois unités mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements.
Il s'agit là d'une tendance, cependant. Les CPU superscalaires tendent à mettre le paquet niveau opérations arithmétiques, mais pas forcément au point de remplir la totalité de la largeur du processeur. La règle est que pour N voies, le processeur dispose d'au moins N/2 ALU entières. Attention, cela ne signifie pas que le processeur émet systématiquement N/2 opération entières. Il peut par exemple émettre une combinaison d'opérations flottantes et mémoire, avec des branchements. Et N/2 est une borne basse, la plupart des CPU x86 ont environ tournent plutôt autour des 2/3, voire des 3/4.
En général, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. La conséquence est que les unités mémoire sont dupliquées, en seconde place dans l'ordre d'importance. Les processeurs x86 ont commencé à dupliquer leurs unités mémoire dès qu'ils ont atteint la quadruple émission, pour les CPU grand public. La microarchitecture Core d'Intel utilisait la quadruple émission, avec deux unités mémoire et trois ALU entières. Les microarchitectures d'AMD K5 et K6 utilisaient aussi la quadruple émission et avaient deux ALUs pour deux unités mémoire.
Plus récemment, la microarchitecture ''Golden Cove'' dispose de cinq unités de calcul trois unités mémoire (deux spécialisées dans les écritures, trois dans les lectures). La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire.Il y a donc soit égalité, soit avantage pour les ALU entières.
Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair.
==L'unité d'émission d'un processeur superscalaire==
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Voyons pourquoi.
===L'excès de ports d'émission===
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ou 5 ?
Une solution est de réduire la largeur du processeur. Dans l'exemple précédent, il est possible de passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La solution précédente est certes très intéressante, mais elle a des défauts. Le principal est que les ports d'émission sont sous-utilisés. Et les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
===L'intérieur de l'unité d'émission===
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Le banc de registre d'un processeur superscalaire===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Une autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions auto-aligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions auto-aligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez longs, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
s38q9s3esx2fya8t8d4mvextasfq88t
773146
773145
2026-09-25T18:53:48Z
Mewtow
31375
/* La double émission entière-flottante */
773146
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===L'impact des contraintes d’appariement sur la performance===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour la suite du chapitre===
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==Le ''front-end'' des processeurs superscalaires==
Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple.
Intuitivement, charger, décoder et 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 certaines processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. 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
|}
Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs.
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
==Les processeurs implémentés avec plusieurs pipelines==
Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Vous vous dites que l'implémentation d'un processeur superscalaire n'est pas la même suivant que le CPU utilise l'exécution dans le désordre, ou non. Mais dans les faits, la distinction est plus subtile que ca. Pour l'expliquer, je vais utiliser une classification originale, qui sépare les processeurs superscalaires en trois sous-types. La distinction se fait sur la manière dont la superscalarité est implémentée en matériel.La classification que j'utilise distingue :
* Les processeurs avec plusieurs pipelines, sans exécution dans le désordre.
* Les processeurs superscalaires avec un pipeline dynamique.
* Les processeurs avec des fenêtres d'instruction décentralisées.
La distinction n'est pas qu'une question d'implémentation matérielle. Elle impacte aussi le ratio entre instructions chargées/décodées et instructions émises. Vous vous dites que les deux sont égales, mais ce n'est le cas que sur les processeurs avec N pipelines ! Sur les autres processeurs, il est possible de décoder 4 instructions, mais d'en émettre maximum 6 par cycle ! Et sur les CPU avec des fenêtres d'instruction décentralisées, il y a aussi une différence entre le nombre d'instructions qui sont ''dispatch'' et ''schedule''.
La catégorie la plus simple est celle des '''processeurs avec plusieurs pipelines'''. Ce sont des processeurs superscalaires sans exécution dans le désordre. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction.
Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, d'une FPU, éventuellement d'un second banc de registres, guère plus. Il permet d'exécuter une seconde opération de calcul, pas plus, ce qui est à l'origine de contraintes d'appariemment assez fortes. De plus, il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section.
===La double émission entière-flottante===
L'exemple le plus simple utilise un pipeline pour les opérations flottantes, séparé du pipeline originel. Pour bien expliquer les choses, prenons le processeur illustré ci-dessous. Un processeur a deux ''back-end'' séparés, un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par l'ALU entière, passent éventuellement dans l'unité mémoire, pour ensuite arriver à l'étape de ''writeback''. La FPU et le banc de registre flottant ont eux aussi leur propre ''back-end'' séparé.
[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]]
Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaqaue pipeline, à chaque cycle.
L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
===La double émission entière===
La seconde classe de CPU superscalaires de l'époque utilisait la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Pour cela, ils peuvent dupliquer une ALU entière, ou reconditionner l'unité mémoire. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 reconditionnait l'unité mémoire en ALU entière.
L'Intel i960 utilisait le reconditionnement de l'AGU en plus du partitionnement, ce qui lui permettait d'exécuter une seconde instruction entière, en plus d'une autre instruction. Les instructions en question étaient simples : additions, quelques décalages. De nos jours, ce combo partitionnement et reconditionnement de l'AGU n'est plus utilisé. Il n'a pas beaucoup de sens quand on peut dupliquer des ALU entières, et les processeurs récents ont un budget en transistor suffisant pour.
Le Pentium dupliquait l'ALU entière, mais n'utilisaient aucune forme de partitionnement. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Contrairement au i960, le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
==Les unités de calcul d'un processeur superscalaire==
Après avoir vu le chargement et le décodage superscalaire, voyons ce qu'il en est au niveau des unités de calcul. Nous allons parler des unités de calcul avant de parler de l'émission, pour une raison simple : l'émission fait l'interface entre unité de calcul et ''front-end'' et on ne peut pas parler de l'émission sans avoir vu comment la superscalarité impacte les deux. Si le ''front-end'' charge et décode un paquet de N instructions, son exécution demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est possible d'exploiter plusieurs unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu.
Les tout premiers CPU superscalaires étaient nombreux à utiliser cette technique. Ils étaient des CPU double émission, avec une ALU entière, une unité mémoire et une FPU. Nous parlerons de '''partitionnement INT-FLOAT-MEM'''. Il s'agit de l'HyperSPARC, de l'alpha 21064 et du PowerPC 603. Les contrainte d’appariement variaient grandement suivant le processeur. Les plus permissifs autorisaient toutes les combinaisons possibles entre : une opération entière, une opération flottante, et une unité mémoire. Mais ils n'étaient pas les seuls.
Le PowerPC 603 implémentait ce genre de double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions).
Il est aussi possible de faire la même chose pour les opérations entières, vu qu'elles sont prises en charge par trois circuits. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres. Il faut parfois lui ajouter le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible d'émettre une opération séparément, ce qui donne le '''partitionnement entier''' dans ce qui suit. Le partitionnement entier n'est jamais utilisé tel quel, car le gain en performance serait trop faible au regard des couts. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Le '''reconditionnement d'une unité fonctionnelle''' permet à celle-ci de faire des opérations qu'elle n'est pas prévue pour. Par exemple, il est possible d'utiliser l'unité mémoire comme seconde ALU entière, ou d'utiliser la FPU comme multiplieur. Ces deux exemples ne sont pas choisit au hasard, nous les étudierons dans ce qui suit. Le reconditionnement évite d'avoir à dupliquer une unité de calcul, tout en ajoutant une voie. Il permet d'améliorer l'efficacité du partitionnement, qu'il soit utilisé seul ou non.
Il est possible de reconditionner la FPU pour faire des multiplications entières. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits. Il est possible d'utiliser de multiplieur pour faire des multiplications entières 32 bits. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
Le reconditionnement de la FPU permet d'exécuter une multiplication entière en plus des autres opérations entières. Par exemple, prenons un CPU avec double émission entière-flottante, sans multiplieur entier séparé. Le reconditionnement de la FPU permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine.
Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants.
Passons maintenant à une seconde forme de reconditionnement. Reconditionner l'unité mémoire permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, des versions améliorées de l'Intel 960. L'Intel 960 originel n'était pas superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire au monde. Il intégrait une ALU entière, une unité mémoire et une unité de branchement. Grâce au partitionnement, il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, en remplacement d'une µop mémoire. Son pipeline permettait donc les combinaisons INT + BRANCH + (MEM / + INT*).
Le cas de l'i960 nous montre que le reconditionnement permet d'améliorer l'intérêt du partitionnement. Elle permet de ne pas dupliquer d'unité de calcul, tout en permettant quand même d'émettre une seconde opération entière.
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''.
Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières ! Il doit y avoir deux ALU entières pour de la double émission entière, trois pour de triple émission éntière, 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]]
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications.
Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que dupliquer l'ALU entière peut se marier parfaitement avec la présence d'une FPU. Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU reconditionne sa FPU ou pas. Mais il est très rare qu'un CPU ait à la fois un multiplieur entier et une FPU reconditionnée.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires modernes utilisent toutes ces stratégies===
Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, duplication de l'ALU entière, duplication des autre unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents.
* Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU.
* Le '''reconditionnement des unités''' a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU.
* La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants.
* La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables.
De nos jours, le partitionnement est utilisé en complément de la duplication des ALU entières, et de la duplication des autres unités. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993.
Le meilleur moyen de voir comment sont combinés ces quatre technique est de regarder ce qu'il s'est passé historiquement.
==L'évolution historique de la superscalarité==
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. La plupart des CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 sont rapidement montés à de la quadruple émission, avec quelques exceptions. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 ||
|-
! 1995
| 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000
|-
! 1996
| class="f_bleu | 620 || || || || class="f_bleu | 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
===Les processeurs historiques à double émission===
Pour commencer, étudions le cas des premiers CPU superscalaires à double émission. Ils sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Notamment, la duplication des autres unités ne sert pour ainsi dire à rien, ce qui limite les possibilités, mais rendra les explications plus simples.
Il pouvaient se classer en deux/trois types principaux, assez stricts, avec quelques processeurs qui ne rentraient pas dans ces cases. Le premier utilise le partitionnement seul, le second dupliquer les ALU entières sans partitionnement, et le troisième type est volontairement passé sous silence pour le moment (nous aborderons le troisième type dans la partie sur des design à quadruple émission).
Les CPU n'utilisant que le partitionnement faisaient la double émission avec trois unités : une ALU, une FPU et une unité mémoire. Le reconditionnement de la FPU était parfois utilisé, mais l'AGU n'était jamais reconditionnée. Si la moitié des processeurs faisait du ''partitionnement complet'' entre ALU, FPU et unité mémoire, l'autre moitié utilisait la bouble émission entière-flottante.
Pour résumer, voici le tableau chronologique disant quel CPU utilisait quelle technique.
* Le partitionnement INT-FLOAT-MEM est en jaune.
* La double émission entière-flottante est en vert.
* La double émission entière est en rouge.
* Les CPU barrés utilisent la quadruple émission et seront vu dans la section suivante.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| class="f_vert | RS/6000 || || || class="f_rouge | i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| class="f_vert | RSC || class="f_vert | PA 7100 || SuperSPARC || || class="f_jaune | 21064 ||
|-
! 1993
| class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || ||
|-
! 1994
| <s>POWER 2</s> || PA 7200 || || || <s>21164</s> ||
|}
Vous voyez que certains processeur sont laissés de côté dans le tableau précédent. C'est car ils ne rentrent pas dans les catégories précédentes et faisaient autrement. Si on omet le Motorola 88110, qui utilisait une forme de double émission assez avancée, les deux CPUs qui vont nous intéresser sont le PA-RISC PA 7200 d'HP, et le SuperSPARC.
Le PA 7200, sorti en 1994, est un PA-7100 auquel on aurait rajouté une ALU entière. Précisons que ce processeur utilisait aussi le reconditionnement de la FPU, comme son prédécesseur. Les combinaisons autorisées étaient donc INT + INT, INT + MEM, INT + FLOAT/MUL, MEM + FLOAT/MUL. Les combinaisons autorisées par le partitionnement INT-FLOAT-MEM étaient donc possibles, en plus des combinaisons INT + INT, voire INT + INT + MUL (vu que la FPU est reconditionnée). Autrement dit, il pouvait faire toute ce que permettait un CPU à double émission entière, plus les combinaisons FLOAT/MUL + MEM.
Le SuperSPARC était un CPU triple émission, avec deux ALU entières, une FPU et une unité mémoire. Il pouvait émettre, à chaque cycle, trois µops en piochant dans deux opérations entières, un accès mémoire, une opération flottante. Le SuperSPARC avait de nombreuses contraintes d’appariement. Par exemple, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Une bonne partie de ces contraintes était lié au banc de registre, qui n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions.
: La seconde ALU a été abandonnée dans l'HyperSPARC, qui est de plus repassé en double émission, ce qui est une évolution à rebours de l'évolution des autres CPUs. Le SuperSPARC était en avance sur son temps, ce quelques années, alors que l'HyperSPARC a un design de son temps.
===Les processeurs historiques à quadruple émission===
C'est la même année , en 1994, que la quadruple émission est arrivée et que les CPU ont commencé à avoir de plus en plus d'unités de calcul. Même les designs restés en double émission avaient ajouté des unités de calcul. Il y avait alors trois stratégies pour la quadruple émission.
La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Les FPUs étaient soit dupliquées, soit scindées en deux en séparant l'additionneur du multiplieur flottant. Le POWER 2 dupliquait ses FPUs à la perfection, alors que l'21264 partitionnait la FPU. Les accès mémoire étaient traités comme des opérations entières, et le processeur pouvait : soit en faire un seul par cycle, soit en émettre deux, sous conditions (le cache devait être double port pour lire/écrire deux données à la fois, comme on le verra dans la suite du chapitre).
Les processeurs MIPS R8000 et R10000 allaient un peu plus loin, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités.
La seconde implémentation dupliquait l'ALU entière et combinait le tout avec un partitionnement INT-FLOAT-MEM. De quoi exécuter en même temps deux opérations entières, une opération flottante, et un accès mémoire. 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).
Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, avec au minimum deux ALU entières. Pour comprendre ce que je veux dire, prenons l'exemple de l'UltraSPARC. Il avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplier flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes. Le processeur pouvait émettre 4 instructions, tant qu'elles atterrissaient dans des unités différentes.
La même méthode peut être utilisée pour la double émission. Le Motorola 88110, sorti en 1991, était un CPU à double émission avec pas moins de 10 unités : deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise.
La morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes. Le tout est résumé dans le tableau ci-dessous, avec :
* en vert les CPU utilisant la première stratégie ;
* en rouge les CPU utilisant la seconde stratégie ;
* en jaune les CPU utilisant la troisième stratégie.
{|class="wikitable"
|+ Processeurs quadruple émission historiques
|-
! Année !! Processeur !! Unités
|-
! 1991
| class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques
|-
|-
! rowspan="2" | 1994
| class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'')
|-
| class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM
|-
! rowspan="3" | 1995
| class="f_jaune | PA 8000 || class="f_jaune | 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD
|-
| class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques
|-
| MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM
|-
! rowspan="2" | 1996
| class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM
|-
| class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL)
|}
===Les processeurs superscalaires modernes===
Les CPU superscalaires modernes ont des design légèrement différents des CPU historiques. Ils dupliquent l'unité entière, mais leur partitionnement est différent. En général, les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Par exemple, un CPU à 6 voies pourra émettre/exécuter 6 opérations entières simultanément, grâce à 6 ALU entières. Mais il s'agit là d'un maximum et certaines peuvent être remplacées par une opération flottante ou mémoire, qui utilisent l'unité mémoire et/ou la FPU.
Un exemple assez parlant est les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon. C'était une microarchitecture à triple émission, avec trois ALU entières et trois unités mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements.
Il s'agit là d'une tendance, cependant. Les CPU superscalaires tendent à mettre le paquet niveau opérations arithmétiques, mais pas forcément au point de remplir la totalité de la largeur du processeur. La règle est que pour N voies, le processeur dispose d'au moins N/2 ALU entières. Attention, cela ne signifie pas que le processeur émet systématiquement N/2 opération entières. Il peut par exemple émettre une combinaison d'opérations flottantes et mémoire, avec des branchements. Et N/2 est une borne basse, la plupart des CPU x86 ont environ tournent plutôt autour des 2/3, voire des 3/4.
En général, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. La conséquence est que les unités mémoire sont dupliquées, en seconde place dans l'ordre d'importance. Les processeurs x86 ont commencé à dupliquer leurs unités mémoire dès qu'ils ont atteint la quadruple émission, pour les CPU grand public. La microarchitecture Core d'Intel utilisait la quadruple émission, avec deux unités mémoire et trois ALU entières. Les microarchitectures d'AMD K5 et K6 utilisaient aussi la quadruple émission et avaient deux ALUs pour deux unités mémoire.
Plus récemment, la microarchitecture ''Golden Cove'' dispose de cinq unités de calcul trois unités mémoire (deux spécialisées dans les écritures, trois dans les lectures). La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire.Il y a donc soit égalité, soit avantage pour les ALU entières.
Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair.
==L'unité d'émission d'un processeur superscalaire==
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Voyons pourquoi.
===L'excès de ports d'émission===
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ou 5 ?
Une solution est de réduire la largeur du processeur. Dans l'exemple précédent, il est possible de passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La solution précédente est certes très intéressante, mais elle a des défauts. Le principal est que les ports d'émission sont sous-utilisés. Et les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
===L'intérieur de l'unité d'émission===
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Le banc de registre d'un processeur superscalaire===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Une autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions auto-aligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions auto-aligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez longs, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
svlbejqv74lmny5aqy56lfluyshicco
773147
773146
2026-09-25T18:55:38Z
Mewtow
31375
/* La double émission entière */
773147
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===L'impact des contraintes d’appariement sur la performance===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour la suite du chapitre===
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==Le ''front-end'' des processeurs superscalaires==
Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple.
Intuitivement, charger, décoder et 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 certaines processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. 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
|}
Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs.
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
==Les processeurs implémentés avec plusieurs pipelines==
Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Vous vous dites que l'implémentation d'un processeur superscalaire n'est pas la même suivant que le CPU utilise l'exécution dans le désordre, ou non. Mais dans les faits, la distinction est plus subtile que ca. Pour l'expliquer, je vais utiliser une classification originale, qui sépare les processeurs superscalaires en trois sous-types. La distinction se fait sur la manière dont la superscalarité est implémentée en matériel.La classification que j'utilise distingue :
* Les processeurs avec plusieurs pipelines, sans exécution dans le désordre.
* Les processeurs superscalaires avec un pipeline dynamique.
* Les processeurs avec des fenêtres d'instruction décentralisées.
La distinction n'est pas qu'une question d'implémentation matérielle. Elle impacte aussi le ratio entre instructions chargées/décodées et instructions émises. Vous vous dites que les deux sont égales, mais ce n'est le cas que sur les processeurs avec N pipelines ! Sur les autres processeurs, il est possible de décoder 4 instructions, mais d'en émettre maximum 6 par cycle ! Et sur les CPU avec des fenêtres d'instruction décentralisées, il y a aussi une différence entre le nombre d'instructions qui sont ''dispatch'' et ''schedule''.
La catégorie la plus simple est celle des '''processeurs avec plusieurs pipelines'''. Ce sont des processeurs superscalaires sans exécution dans le désordre. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction.
Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, d'une FPU, éventuellement d'un second banc de registres, guère plus. Il permet d'exécuter une seconde opération de calcul, pas plus, ce qui est à l'origine de contraintes d'appariemment assez fortes. De plus, il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section.
===La double émission entière-flottante===
L'exemple le plus simple utilise un pipeline pour les opérations flottantes, séparé du pipeline originel. Pour bien expliquer les choses, prenons le processeur illustré ci-dessous. Un processeur a deux ''back-end'' séparés, un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par l'ALU entière, passent éventuellement dans l'unité mémoire, pour ensuite arriver à l'étape de ''writeback''. La FPU et le banc de registre flottant ont eux aussi leur propre ''back-end'' séparé.
[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]]
Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaqaue pipeline, à chaque cycle.
L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
===La double émission entière===
Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 réutilisait l'unité de calcul comme seconde unité de calcul d'adresse.
L'Intel i960 utilisait le reconditionnement de l'AGU en plus du partitionnement, ce qui lui permettait d'exécuter une seconde instruction entière, en plus d'une autre instruction. Les instructions en question étaient simples : additions, quelques décalages. De nos jours, ce combo partitionnement et reconditionnement de l'AGU n'est plus utilisé. Il n'a pas beaucoup de sens quand on peut dupliquer des ALU entières, et les processeurs récents ont un budget en transistor suffisant pour.
Le Pentium dupliquait l'ALU entière, mais n'utilisaient aucune forme de partitionnement. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Contrairement au i960, le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
==Les unités de calcul d'un processeur superscalaire==
Après avoir vu le chargement et le décodage superscalaire, voyons ce qu'il en est au niveau des unités de calcul. Nous allons parler des unités de calcul avant de parler de l'émission, pour une raison simple : l'émission fait l'interface entre unité de calcul et ''front-end'' et on ne peut pas parler de l'émission sans avoir vu comment la superscalarité impacte les deux. Si le ''front-end'' charge et décode un paquet de N instructions, son exécution demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est possible d'exploiter plusieurs unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu.
Les tout premiers CPU superscalaires étaient nombreux à utiliser cette technique. Ils étaient des CPU double émission, avec une ALU entière, une unité mémoire et une FPU. Nous parlerons de '''partitionnement INT-FLOAT-MEM'''. Il s'agit de l'HyperSPARC, de l'alpha 21064 et du PowerPC 603. Les contrainte d’appariement variaient grandement suivant le processeur. Les plus permissifs autorisaient toutes les combinaisons possibles entre : une opération entière, une opération flottante, et une unité mémoire. Mais ils n'étaient pas les seuls.
Le PowerPC 603 implémentait ce genre de double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions).
Il est aussi possible de faire la même chose pour les opérations entières, vu qu'elles sont prises en charge par trois circuits. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres. Il faut parfois lui ajouter le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible d'émettre une opération séparément, ce qui donne le '''partitionnement entier''' dans ce qui suit. Le partitionnement entier n'est jamais utilisé tel quel, car le gain en performance serait trop faible au regard des couts. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Le '''reconditionnement d'une unité fonctionnelle''' permet à celle-ci de faire des opérations qu'elle n'est pas prévue pour. Par exemple, il est possible d'utiliser l'unité mémoire comme seconde ALU entière, ou d'utiliser la FPU comme multiplieur. Ces deux exemples ne sont pas choisit au hasard, nous les étudierons dans ce qui suit. Le reconditionnement évite d'avoir à dupliquer une unité de calcul, tout en ajoutant une voie. Il permet d'améliorer l'efficacité du partitionnement, qu'il soit utilisé seul ou non.
Il est possible de reconditionner la FPU pour faire des multiplications entières. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits. Il est possible d'utiliser de multiplieur pour faire des multiplications entières 32 bits. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
Le reconditionnement de la FPU permet d'exécuter une multiplication entière en plus des autres opérations entières. Par exemple, prenons un CPU avec double émission entière-flottante, sans multiplieur entier séparé. Le reconditionnement de la FPU permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine.
Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants.
Passons maintenant à une seconde forme de reconditionnement. Reconditionner l'unité mémoire permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, des versions améliorées de l'Intel 960. L'Intel 960 originel n'était pas superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire au monde. Il intégrait une ALU entière, une unité mémoire et une unité de branchement. Grâce au partitionnement, il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, en remplacement d'une µop mémoire. Son pipeline permettait donc les combinaisons INT + BRANCH + (MEM / + INT*).
Le cas de l'i960 nous montre que le reconditionnement permet d'améliorer l'intérêt du partitionnement. Elle permet de ne pas dupliquer d'unité de calcul, tout en permettant quand même d'émettre une seconde opération entière.
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''.
Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières ! Il doit y avoir deux ALU entières pour de la double émission entière, trois pour de triple émission éntière, 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]]
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications.
Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que dupliquer l'ALU entière peut se marier parfaitement avec la présence d'une FPU. Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU reconditionne sa FPU ou pas. Mais il est très rare qu'un CPU ait à la fois un multiplieur entier et une FPU reconditionnée.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires modernes utilisent toutes ces stratégies===
Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, duplication de l'ALU entière, duplication des autre unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents.
* Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU.
* Le '''reconditionnement des unités''' a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU.
* La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants.
* La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables.
De nos jours, le partitionnement est utilisé en complément de la duplication des ALU entières, et de la duplication des autres unités. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993.
Le meilleur moyen de voir comment sont combinés ces quatre technique est de regarder ce qu'il s'est passé historiquement.
==L'évolution historique de la superscalarité==
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. La plupart des CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 sont rapidement montés à de la quadruple émission, avec quelques exceptions. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 ||
|-
! 1995
| 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000
|-
! 1996
| class="f_bleu | 620 || || || || class="f_bleu | 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
===Les processeurs historiques à double émission===
Pour commencer, étudions le cas des premiers CPU superscalaires à double émission. Ils sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Notamment, la duplication des autres unités ne sert pour ainsi dire à rien, ce qui limite les possibilités, mais rendra les explications plus simples.
Il pouvaient se classer en deux/trois types principaux, assez stricts, avec quelques processeurs qui ne rentraient pas dans ces cases. Le premier utilise le partitionnement seul, le second dupliquer les ALU entières sans partitionnement, et le troisième type est volontairement passé sous silence pour le moment (nous aborderons le troisième type dans la partie sur des design à quadruple émission).
Les CPU n'utilisant que le partitionnement faisaient la double émission avec trois unités : une ALU, une FPU et une unité mémoire. Le reconditionnement de la FPU était parfois utilisé, mais l'AGU n'était jamais reconditionnée. Si la moitié des processeurs faisait du ''partitionnement complet'' entre ALU, FPU et unité mémoire, l'autre moitié utilisait la bouble émission entière-flottante.
Pour résumer, voici le tableau chronologique disant quel CPU utilisait quelle technique.
* Le partitionnement INT-FLOAT-MEM est en jaune.
* La double émission entière-flottante est en vert.
* La double émission entière est en rouge.
* Les CPU barrés utilisent la quadruple émission et seront vu dans la section suivante.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| class="f_vert | RS/6000 || || || class="f_rouge | i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| class="f_vert | RSC || class="f_vert | PA 7100 || SuperSPARC || || class="f_jaune | 21064 ||
|-
! 1993
| class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || ||
|-
! 1994
| <s>POWER 2</s> || PA 7200 || || || <s>21164</s> ||
|}
Vous voyez que certains processeur sont laissés de côté dans le tableau précédent. C'est car ils ne rentrent pas dans les catégories précédentes et faisaient autrement. Si on omet le Motorola 88110, qui utilisait une forme de double émission assez avancée, les deux CPUs qui vont nous intéresser sont le PA-RISC PA 7200 d'HP, et le SuperSPARC.
Le PA 7200, sorti en 1994, est un PA-7100 auquel on aurait rajouté une ALU entière. Précisons que ce processeur utilisait aussi le reconditionnement de la FPU, comme son prédécesseur. Les combinaisons autorisées étaient donc INT + INT, INT + MEM, INT + FLOAT/MUL, MEM + FLOAT/MUL. Les combinaisons autorisées par le partitionnement INT-FLOAT-MEM étaient donc possibles, en plus des combinaisons INT + INT, voire INT + INT + MUL (vu que la FPU est reconditionnée). Autrement dit, il pouvait faire toute ce que permettait un CPU à double émission entière, plus les combinaisons FLOAT/MUL + MEM.
Le SuperSPARC était un CPU triple émission, avec deux ALU entières, une FPU et une unité mémoire. Il pouvait émettre, à chaque cycle, trois µops en piochant dans deux opérations entières, un accès mémoire, une opération flottante. Le SuperSPARC avait de nombreuses contraintes d’appariement. Par exemple, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Une bonne partie de ces contraintes était lié au banc de registre, qui n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions.
: La seconde ALU a été abandonnée dans l'HyperSPARC, qui est de plus repassé en double émission, ce qui est une évolution à rebours de l'évolution des autres CPUs. Le SuperSPARC était en avance sur son temps, ce quelques années, alors que l'HyperSPARC a un design de son temps.
===Les processeurs historiques à quadruple émission===
C'est la même année , en 1994, que la quadruple émission est arrivée et que les CPU ont commencé à avoir de plus en plus d'unités de calcul. Même les designs restés en double émission avaient ajouté des unités de calcul. Il y avait alors trois stratégies pour la quadruple émission.
La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Les FPUs étaient soit dupliquées, soit scindées en deux en séparant l'additionneur du multiplieur flottant. Le POWER 2 dupliquait ses FPUs à la perfection, alors que l'21264 partitionnait la FPU. Les accès mémoire étaient traités comme des opérations entières, et le processeur pouvait : soit en faire un seul par cycle, soit en émettre deux, sous conditions (le cache devait être double port pour lire/écrire deux données à la fois, comme on le verra dans la suite du chapitre).
Les processeurs MIPS R8000 et R10000 allaient un peu plus loin, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités.
La seconde implémentation dupliquait l'ALU entière et combinait le tout avec un partitionnement INT-FLOAT-MEM. De quoi exécuter en même temps deux opérations entières, une opération flottante, et un accès mémoire. 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).
Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, avec au minimum deux ALU entières. Pour comprendre ce que je veux dire, prenons l'exemple de l'UltraSPARC. Il avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplier flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes. Le processeur pouvait émettre 4 instructions, tant qu'elles atterrissaient dans des unités différentes.
La même méthode peut être utilisée pour la double émission. Le Motorola 88110, sorti en 1991, était un CPU à double émission avec pas moins de 10 unités : deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise.
La morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes. Le tout est résumé dans le tableau ci-dessous, avec :
* en vert les CPU utilisant la première stratégie ;
* en rouge les CPU utilisant la seconde stratégie ;
* en jaune les CPU utilisant la troisième stratégie.
{|class="wikitable"
|+ Processeurs quadruple émission historiques
|-
! Année !! Processeur !! Unités
|-
! 1991
| class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques
|-
|-
! rowspan="2" | 1994
| class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'')
|-
| class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM
|-
! rowspan="3" | 1995
| class="f_jaune | PA 8000 || class="f_jaune | 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD
|-
| class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques
|-
| MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM
|-
! rowspan="2" | 1996
| class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM
|-
| class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL)
|}
===Les processeurs superscalaires modernes===
Les CPU superscalaires modernes ont des design légèrement différents des CPU historiques. Ils dupliquent l'unité entière, mais leur partitionnement est différent. En général, les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Par exemple, un CPU à 6 voies pourra émettre/exécuter 6 opérations entières simultanément, grâce à 6 ALU entières. Mais il s'agit là d'un maximum et certaines peuvent être remplacées par une opération flottante ou mémoire, qui utilisent l'unité mémoire et/ou la FPU.
Un exemple assez parlant est les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon. C'était une microarchitecture à triple émission, avec trois ALU entières et trois unités mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements.
Il s'agit là d'une tendance, cependant. Les CPU superscalaires tendent à mettre le paquet niveau opérations arithmétiques, mais pas forcément au point de remplir la totalité de la largeur du processeur. La règle est que pour N voies, le processeur dispose d'au moins N/2 ALU entières. Attention, cela ne signifie pas que le processeur émet systématiquement N/2 opération entières. Il peut par exemple émettre une combinaison d'opérations flottantes et mémoire, avec des branchements. Et N/2 est une borne basse, la plupart des CPU x86 ont environ tournent plutôt autour des 2/3, voire des 3/4.
En général, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. La conséquence est que les unités mémoire sont dupliquées, en seconde place dans l'ordre d'importance. Les processeurs x86 ont commencé à dupliquer leurs unités mémoire dès qu'ils ont atteint la quadruple émission, pour les CPU grand public. La microarchitecture Core d'Intel utilisait la quadruple émission, avec deux unités mémoire et trois ALU entières. Les microarchitectures d'AMD K5 et K6 utilisaient aussi la quadruple émission et avaient deux ALUs pour deux unités mémoire.
Plus récemment, la microarchitecture ''Golden Cove'' dispose de cinq unités de calcul trois unités mémoire (deux spécialisées dans les écritures, trois dans les lectures). La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire.Il y a donc soit égalité, soit avantage pour les ALU entières.
Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair.
==L'unité d'émission d'un processeur superscalaire==
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Voyons pourquoi.
===L'excès de ports d'émission===
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ou 5 ?
Une solution est de réduire la largeur du processeur. Dans l'exemple précédent, il est possible de passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La solution précédente est certes très intéressante, mais elle a des défauts. Le principal est que les ports d'émission sont sous-utilisés. Et les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
===L'intérieur de l'unité d'émission===
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Le banc de registre d'un processeur superscalaire===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Une autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions auto-aligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions auto-aligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez longs, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
5qxidnoqvcnkwzc6ky5pkxwcf585kuk
773148
773147
2026-09-25T18:57:51Z
Mewtow
31375
/* Le reconditionnement des unités fonctionnelles */
773148
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===L'impact des contraintes d’appariement sur la performance===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour la suite du chapitre===
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==Le ''front-end'' des processeurs superscalaires==
Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple.
Intuitivement, charger, décoder et 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 certaines processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. 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
|}
Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs.
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
==Les processeurs implémentés avec plusieurs pipelines==
Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Vous vous dites que l'implémentation d'un processeur superscalaire n'est pas la même suivant que le CPU utilise l'exécution dans le désordre, ou non. Mais dans les faits, la distinction est plus subtile que ca. Pour l'expliquer, je vais utiliser une classification originale, qui sépare les processeurs superscalaires en trois sous-types. La distinction se fait sur la manière dont la superscalarité est implémentée en matériel.La classification que j'utilise distingue :
* Les processeurs avec plusieurs pipelines, sans exécution dans le désordre.
* Les processeurs superscalaires avec un pipeline dynamique.
* Les processeurs avec des fenêtres d'instruction décentralisées.
La distinction n'est pas qu'une question d'implémentation matérielle. Elle impacte aussi le ratio entre instructions chargées/décodées et instructions émises. Vous vous dites que les deux sont égales, mais ce n'est le cas que sur les processeurs avec N pipelines ! Sur les autres processeurs, il est possible de décoder 4 instructions, mais d'en émettre maximum 6 par cycle ! Et sur les CPU avec des fenêtres d'instruction décentralisées, il y a aussi une différence entre le nombre d'instructions qui sont ''dispatch'' et ''schedule''.
La catégorie la plus simple est celle des '''processeurs avec plusieurs pipelines'''. Ce sont des processeurs superscalaires sans exécution dans le désordre. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction.
Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, d'une FPU, éventuellement d'un second banc de registres, guère plus. Il permet d'exécuter une seconde opération de calcul, pas plus, ce qui est à l'origine de contraintes d'appariemment assez fortes. De plus, il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section.
===La double émission entière-flottante===
L'exemple le plus simple utilise un pipeline pour les opérations flottantes, séparé du pipeline originel. Pour bien expliquer les choses, prenons le processeur illustré ci-dessous. Un processeur a deux ''back-end'' séparés, un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par l'ALU entière, passent éventuellement dans l'unité mémoire, pour ensuite arriver à l'étape de ''writeback''. La FPU et le banc de registre flottant ont eux aussi leur propre ''back-end'' séparé.
[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]]
Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaqaue pipeline, à chaque cycle.
L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
===La double émission entière===
Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 réutilisait l'unité de calcul comme seconde unité de calcul d'adresse.
L'Intel i960 utilisait le reconditionnement de l'AGU en plus du partitionnement, ce qui lui permettait d'exécuter une seconde instruction entière, en plus d'une autre instruction. Les instructions en question étaient simples : additions, quelques décalages. De nos jours, ce combo partitionnement et reconditionnement de l'AGU n'est plus utilisé. Il n'a pas beaucoup de sens quand on peut dupliquer des ALU entières, et les processeurs récents ont un budget en transistor suffisant pour.
Le Pentium dupliquait l'ALU entière, mais n'utilisaient aucune forme de partitionnement. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Contrairement au i960, le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
==Les unités de calcul d'un processeur superscalaire==
Après avoir vu le chargement et le décodage superscalaire, voyons ce qu'il en est au niveau des unités de calcul. Nous allons parler des unités de calcul avant de parler de l'émission, pour une raison simple : l'émission fait l'interface entre unité de calcul et ''front-end'' et on ne peut pas parler de l'émission sans avoir vu comment la superscalarité impacte les deux. Si le ''front-end'' charge et décode un paquet de N instructions, son exécution demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est possible d'exploiter plusieurs unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu.
Les tout premiers CPU superscalaires étaient nombreux à utiliser cette technique. Ils étaient des CPU double émission, avec une ALU entière, une unité mémoire et une FPU. Nous parlerons de '''partitionnement INT-FLOAT-MEM'''. Il s'agit de l'HyperSPARC, de l'alpha 21064 et du PowerPC 603. Les contrainte d’appariement variaient grandement suivant le processeur. Les plus permissifs autorisaient toutes les combinaisons possibles entre : une opération entière, une opération flottante, et une unité mémoire. Mais ils n'étaient pas les seuls.
Le PowerPC 603 implémentait ce genre de double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions).
Il est aussi possible de faire la même chose pour les opérations entières, vu qu'elles sont prises en charge par trois circuits. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres. Il faut parfois lui ajouter le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible d'émettre une opération séparément, ce qui donne le '''partitionnement entier''' dans ce qui suit. Le partitionnement entier n'est jamais utilisé tel quel, car le gain en performance serait trop faible au regard des couts. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Le '''reconditionnement d'une unité fonctionnelle''' permet à celle-ci de faire des opérations qu'elle n'est pas prévue pour. Par exemple, il est possible d'utiliser l'unité mémoire comme seconde ALU entière, ou d'utiliser la FPU comme multiplieur. Ces deux exemples ne sont pas choisit au hasard, nous les étudierons dans ce qui suit. Le reconditionnement évite d'avoir à dupliquer une unité de calcul, tout en ajoutant une voie. Il permet d'améliorer l'efficacité du partitionnement, qu'il soit utilisé seul ou non.
Il est possible de reconditionner la FPU pour faire des multiplications entières. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits. Il est possible d'utiliser de multiplieur pour faire des multiplications entières 32 bits. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
Le reconditionnement de la FPU permet d'exécuter une multiplication entière en plus des autres opérations entières. Par exemple, prenons un CPU avec double émission entière-flottante, sans multiplieur entier séparé. Le reconditionnement de la FPU permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine.
Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants.
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''.
Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières ! Il doit y avoir deux ALU entières pour de la double émission entière, trois pour de triple émission éntière, 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]]
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications.
Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que dupliquer l'ALU entière peut se marier parfaitement avec la présence d'une FPU. Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU reconditionne sa FPU ou pas. Mais il est très rare qu'un CPU ait à la fois un multiplieur entier et une FPU reconditionnée.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires modernes utilisent toutes ces stratégies===
Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, duplication de l'ALU entière, duplication des autre unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents.
* Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU.
* Le '''reconditionnement des unités''' a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU.
* La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants.
* La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables.
De nos jours, le partitionnement est utilisé en complément de la duplication des ALU entières, et de la duplication des autres unités. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993.
Le meilleur moyen de voir comment sont combinés ces quatre technique est de regarder ce qu'il s'est passé historiquement.
==L'évolution historique de la superscalarité==
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. La plupart des CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 sont rapidement montés à de la quadruple émission, avec quelques exceptions. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 ||
|-
! 1995
| 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000
|-
! 1996
| class="f_bleu | 620 || || || || class="f_bleu | 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
===Les processeurs historiques à double émission===
Pour commencer, étudions le cas des premiers CPU superscalaires à double émission. Ils sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Notamment, la duplication des autres unités ne sert pour ainsi dire à rien, ce qui limite les possibilités, mais rendra les explications plus simples.
Il pouvaient se classer en deux/trois types principaux, assez stricts, avec quelques processeurs qui ne rentraient pas dans ces cases. Le premier utilise le partitionnement seul, le second dupliquer les ALU entières sans partitionnement, et le troisième type est volontairement passé sous silence pour le moment (nous aborderons le troisième type dans la partie sur des design à quadruple émission).
Les CPU n'utilisant que le partitionnement faisaient la double émission avec trois unités : une ALU, une FPU et une unité mémoire. Le reconditionnement de la FPU était parfois utilisé, mais l'AGU n'était jamais reconditionnée. Si la moitié des processeurs faisait du ''partitionnement complet'' entre ALU, FPU et unité mémoire, l'autre moitié utilisait la bouble émission entière-flottante.
Pour résumer, voici le tableau chronologique disant quel CPU utilisait quelle technique.
* Le partitionnement INT-FLOAT-MEM est en jaune.
* La double émission entière-flottante est en vert.
* La double émission entière est en rouge.
* Les CPU barrés utilisent la quadruple émission et seront vu dans la section suivante.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| class="f_vert | RS/6000 || || || class="f_rouge | i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| class="f_vert | RSC || class="f_vert | PA 7100 || SuperSPARC || || class="f_jaune | 21064 ||
|-
! 1993
| class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || ||
|-
! 1994
| <s>POWER 2</s> || PA 7200 || || || <s>21164</s> ||
|}
Vous voyez que certains processeur sont laissés de côté dans le tableau précédent. C'est car ils ne rentrent pas dans les catégories précédentes et faisaient autrement. Si on omet le Motorola 88110, qui utilisait une forme de double émission assez avancée, les deux CPUs qui vont nous intéresser sont le PA-RISC PA 7200 d'HP, et le SuperSPARC.
Le PA 7200, sorti en 1994, est un PA-7100 auquel on aurait rajouté une ALU entière. Précisons que ce processeur utilisait aussi le reconditionnement de la FPU, comme son prédécesseur. Les combinaisons autorisées étaient donc INT + INT, INT + MEM, INT + FLOAT/MUL, MEM + FLOAT/MUL. Les combinaisons autorisées par le partitionnement INT-FLOAT-MEM étaient donc possibles, en plus des combinaisons INT + INT, voire INT + INT + MUL (vu que la FPU est reconditionnée). Autrement dit, il pouvait faire toute ce que permettait un CPU à double émission entière, plus les combinaisons FLOAT/MUL + MEM.
Le SuperSPARC était un CPU triple émission, avec deux ALU entières, une FPU et une unité mémoire. Il pouvait émettre, à chaque cycle, trois µops en piochant dans deux opérations entières, un accès mémoire, une opération flottante. Le SuperSPARC avait de nombreuses contraintes d’appariement. Par exemple, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Une bonne partie de ces contraintes était lié au banc de registre, qui n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions.
: La seconde ALU a été abandonnée dans l'HyperSPARC, qui est de plus repassé en double émission, ce qui est une évolution à rebours de l'évolution des autres CPUs. Le SuperSPARC était en avance sur son temps, ce quelques années, alors que l'HyperSPARC a un design de son temps.
===Les processeurs historiques à quadruple émission===
C'est la même année , en 1994, que la quadruple émission est arrivée et que les CPU ont commencé à avoir de plus en plus d'unités de calcul. Même les designs restés en double émission avaient ajouté des unités de calcul. Il y avait alors trois stratégies pour la quadruple émission.
La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Les FPUs étaient soit dupliquées, soit scindées en deux en séparant l'additionneur du multiplieur flottant. Le POWER 2 dupliquait ses FPUs à la perfection, alors que l'21264 partitionnait la FPU. Les accès mémoire étaient traités comme des opérations entières, et le processeur pouvait : soit en faire un seul par cycle, soit en émettre deux, sous conditions (le cache devait être double port pour lire/écrire deux données à la fois, comme on le verra dans la suite du chapitre).
Les processeurs MIPS R8000 et R10000 allaient un peu plus loin, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités.
La seconde implémentation dupliquait l'ALU entière et combinait le tout avec un partitionnement INT-FLOAT-MEM. De quoi exécuter en même temps deux opérations entières, une opération flottante, et un accès mémoire. 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).
Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, avec au minimum deux ALU entières. Pour comprendre ce que je veux dire, prenons l'exemple de l'UltraSPARC. Il avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplier flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes. Le processeur pouvait émettre 4 instructions, tant qu'elles atterrissaient dans des unités différentes.
La même méthode peut être utilisée pour la double émission. Le Motorola 88110, sorti en 1991, était un CPU à double émission avec pas moins de 10 unités : deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise.
La morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes. Le tout est résumé dans le tableau ci-dessous, avec :
* en vert les CPU utilisant la première stratégie ;
* en rouge les CPU utilisant la seconde stratégie ;
* en jaune les CPU utilisant la troisième stratégie.
{|class="wikitable"
|+ Processeurs quadruple émission historiques
|-
! Année !! Processeur !! Unités
|-
! 1991
| class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques
|-
|-
! rowspan="2" | 1994
| class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'')
|-
| class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM
|-
! rowspan="3" | 1995
| class="f_jaune | PA 8000 || class="f_jaune | 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD
|-
| class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques
|-
| MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM
|-
! rowspan="2" | 1996
| class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM
|-
| class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL)
|}
===Les processeurs superscalaires modernes===
Les CPU superscalaires modernes ont des design légèrement différents des CPU historiques. Ils dupliquent l'unité entière, mais leur partitionnement est différent. En général, les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Par exemple, un CPU à 6 voies pourra émettre/exécuter 6 opérations entières simultanément, grâce à 6 ALU entières. Mais il s'agit là d'un maximum et certaines peuvent être remplacées par une opération flottante ou mémoire, qui utilisent l'unité mémoire et/ou la FPU.
Un exemple assez parlant est les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon. C'était une microarchitecture à triple émission, avec trois ALU entières et trois unités mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements.
Il s'agit là d'une tendance, cependant. Les CPU superscalaires tendent à mettre le paquet niveau opérations arithmétiques, mais pas forcément au point de remplir la totalité de la largeur du processeur. La règle est que pour N voies, le processeur dispose d'au moins N/2 ALU entières. Attention, cela ne signifie pas que le processeur émet systématiquement N/2 opération entières. Il peut par exemple émettre une combinaison d'opérations flottantes et mémoire, avec des branchements. Et N/2 est une borne basse, la plupart des CPU x86 ont environ tournent plutôt autour des 2/3, voire des 3/4.
En général, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. La conséquence est que les unités mémoire sont dupliquées, en seconde place dans l'ordre d'importance. Les processeurs x86 ont commencé à dupliquer leurs unités mémoire dès qu'ils ont atteint la quadruple émission, pour les CPU grand public. La microarchitecture Core d'Intel utilisait la quadruple émission, avec deux unités mémoire et trois ALU entières. Les microarchitectures d'AMD K5 et K6 utilisaient aussi la quadruple émission et avaient deux ALUs pour deux unités mémoire.
Plus récemment, la microarchitecture ''Golden Cove'' dispose de cinq unités de calcul trois unités mémoire (deux spécialisées dans les écritures, trois dans les lectures). La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire.Il y a donc soit égalité, soit avantage pour les ALU entières.
Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair.
==L'unité d'émission d'un processeur superscalaire==
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Voyons pourquoi.
===L'excès de ports d'émission===
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ou 5 ?
Une solution est de réduire la largeur du processeur. Dans l'exemple précédent, il est possible de passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La solution précédente est certes très intéressante, mais elle a des défauts. Le principal est que les ports d'émission sont sous-utilisés. Et les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
===L'intérieur de l'unité d'émission===
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Le banc de registre d'un processeur superscalaire===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Une autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions auto-aligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions auto-aligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez longs, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
7oz99fkbxk238r3ouj88t1539hcd1sq
773149
773148
2026-09-25T19:03:21Z
Mewtow
31375
/* La double émission entière */
773149
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===L'impact des contraintes d’appariement sur la performance===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour la suite du chapitre===
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==Le ''front-end'' des processeurs superscalaires==
Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple.
Intuitivement, charger, décoder et 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 certaines processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. 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
|}
Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs.
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
==Les processeurs implémentés avec plusieurs pipelines==
Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Vous vous dites que l'implémentation d'un processeur superscalaire n'est pas la même suivant que le CPU utilise l'exécution dans le désordre, ou non. Mais dans les faits, la distinction est plus subtile que ca. Pour l'expliquer, je vais utiliser une classification originale, qui sépare les processeurs superscalaires en trois sous-types. La distinction se fait sur la manière dont la superscalarité est implémentée en matériel.La classification que j'utilise distingue :
* Les processeurs avec plusieurs pipelines, sans exécution dans le désordre.
* Les processeurs superscalaires avec un pipeline dynamique.
* Les processeurs avec des fenêtres d'instruction décentralisées.
La distinction n'est pas qu'une question d'implémentation matérielle. Elle impacte aussi le ratio entre instructions chargées/décodées et instructions émises. Vous vous dites que les deux sont égales, mais ce n'est le cas que sur les processeurs avec N pipelines ! Sur les autres processeurs, il est possible de décoder 4 instructions, mais d'en émettre maximum 6 par cycle ! Et sur les CPU avec des fenêtres d'instruction décentralisées, il y a aussi une différence entre le nombre d'instructions qui sont ''dispatch'' et ''schedule''.
La catégorie la plus simple est celle des '''processeurs avec plusieurs pipelines'''. Ce sont des processeurs superscalaires sans exécution dans le désordre. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction.
Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, d'une FPU, éventuellement d'un second banc de registres, guère plus. Il permet d'exécuter une seconde opération de calcul, pas plus, ce qui est à l'origine de contraintes d'appariemment assez fortes. De plus, il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section.
===La double émission entière-flottante===
L'exemple le plus simple utilise un pipeline pour les opérations flottantes, séparé du pipeline originel. Pour bien expliquer les choses, prenons le processeur illustré ci-dessous. Un processeur a deux ''back-end'' séparés, un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par l'ALU entière, passent éventuellement dans l'unité mémoire, pour ensuite arriver à l'étape de ''writeback''. La FPU et le banc de registre flottant ont eux aussi leur propre ''back-end'' séparé.
[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]]
Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaqaue pipeline, à chaque cycle.
L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
===La double émission entière===
Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait.
Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
==Les unités de calcul d'un processeur superscalaire==
Après avoir vu le chargement et le décodage superscalaire, voyons ce qu'il en est au niveau des unités de calcul. Nous allons parler des unités de calcul avant de parler de l'émission, pour une raison simple : l'émission fait l'interface entre unité de calcul et ''front-end'' et on ne peut pas parler de l'émission sans avoir vu comment la superscalarité impacte les deux. Si le ''front-end'' charge et décode un paquet de N instructions, son exécution demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est possible d'exploiter plusieurs unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu.
Les tout premiers CPU superscalaires étaient nombreux à utiliser cette technique. Ils étaient des CPU double émission, avec une ALU entière, une unité mémoire et une FPU. Nous parlerons de '''partitionnement INT-FLOAT-MEM'''. Il s'agit de l'HyperSPARC, de l'alpha 21064 et du PowerPC 603. Les contrainte d’appariement variaient grandement suivant le processeur. Les plus permissifs autorisaient toutes les combinaisons possibles entre : une opération entière, une opération flottante, et une unité mémoire. Mais ils n'étaient pas les seuls.
Le PowerPC 603 implémentait ce genre de double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions).
Il est aussi possible de faire la même chose pour les opérations entières, vu qu'elles sont prises en charge par trois circuits. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres. Il faut parfois lui ajouter le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible d'émettre une opération séparément, ce qui donne le '''partitionnement entier''' dans ce qui suit. Le partitionnement entier n'est jamais utilisé tel quel, car le gain en performance serait trop faible au regard des couts. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Le '''reconditionnement d'une unité fonctionnelle''' permet à celle-ci de faire des opérations qu'elle n'est pas prévue pour. Par exemple, il est possible d'utiliser l'unité mémoire comme seconde ALU entière, ou d'utiliser la FPU comme multiplieur. Ces deux exemples ne sont pas choisit au hasard, nous les étudierons dans ce qui suit. Le reconditionnement évite d'avoir à dupliquer une unité de calcul, tout en ajoutant une voie. Il permet d'améliorer l'efficacité du partitionnement, qu'il soit utilisé seul ou non.
Il est possible de reconditionner la FPU pour faire des multiplications entières. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits. Il est possible d'utiliser de multiplieur pour faire des multiplications entières 32 bits. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
Le reconditionnement de la FPU permet d'exécuter une multiplication entière en plus des autres opérations entières. Par exemple, prenons un CPU avec double émission entière-flottante, sans multiplieur entier séparé. Le reconditionnement de la FPU permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine.
Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants.
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''.
Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières ! Il doit y avoir deux ALU entières pour de la double émission entière, trois pour de triple émission éntière, 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]]
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications.
Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que dupliquer l'ALU entière peut se marier parfaitement avec la présence d'une FPU. Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU reconditionne sa FPU ou pas. Mais il est très rare qu'un CPU ait à la fois un multiplieur entier et une FPU reconditionnée.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires modernes utilisent toutes ces stratégies===
Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, duplication de l'ALU entière, duplication des autre unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents.
* Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU.
* Le '''reconditionnement des unités''' a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU.
* La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants.
* La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables.
De nos jours, le partitionnement est utilisé en complément de la duplication des ALU entières, et de la duplication des autres unités. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993.
Le meilleur moyen de voir comment sont combinés ces quatre technique est de regarder ce qu'il s'est passé historiquement.
==L'évolution historique de la superscalarité==
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. La plupart des CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 sont rapidement montés à de la quadruple émission, avec quelques exceptions. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 ||
|-
! 1995
| 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000
|-
! 1996
| class="f_bleu | 620 || || || || class="f_bleu | 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
===Les processeurs historiques à double émission===
Pour commencer, étudions le cas des premiers CPU superscalaires à double émission. Ils sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Notamment, la duplication des autres unités ne sert pour ainsi dire à rien, ce qui limite les possibilités, mais rendra les explications plus simples.
Il pouvaient se classer en deux/trois types principaux, assez stricts, avec quelques processeurs qui ne rentraient pas dans ces cases. Le premier utilise le partitionnement seul, le second dupliquer les ALU entières sans partitionnement, et le troisième type est volontairement passé sous silence pour le moment (nous aborderons le troisième type dans la partie sur des design à quadruple émission).
Les CPU n'utilisant que le partitionnement faisaient la double émission avec trois unités : une ALU, une FPU et une unité mémoire. Le reconditionnement de la FPU était parfois utilisé, mais l'AGU n'était jamais reconditionnée. Si la moitié des processeurs faisait du ''partitionnement complet'' entre ALU, FPU et unité mémoire, l'autre moitié utilisait la bouble émission entière-flottante.
Pour résumer, voici le tableau chronologique disant quel CPU utilisait quelle technique.
* Le partitionnement INT-FLOAT-MEM est en jaune.
* La double émission entière-flottante est en vert.
* La double émission entière est en rouge.
* Les CPU barrés utilisent la quadruple émission et seront vu dans la section suivante.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| class="f_vert | RS/6000 || || || class="f_rouge | i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| class="f_vert | RSC || class="f_vert | PA 7100 || SuperSPARC || || class="f_jaune | 21064 ||
|-
! 1993
| class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || ||
|-
! 1994
| <s>POWER 2</s> || PA 7200 || || || <s>21164</s> ||
|}
Vous voyez que certains processeur sont laissés de côté dans le tableau précédent. C'est car ils ne rentrent pas dans les catégories précédentes et faisaient autrement. Si on omet le Motorola 88110, qui utilisait une forme de double émission assez avancée, les deux CPUs qui vont nous intéresser sont le PA-RISC PA 7200 d'HP, et le SuperSPARC.
Le PA 7200, sorti en 1994, est un PA-7100 auquel on aurait rajouté une ALU entière. Précisons que ce processeur utilisait aussi le reconditionnement de la FPU, comme son prédécesseur. Les combinaisons autorisées étaient donc INT + INT, INT + MEM, INT + FLOAT/MUL, MEM + FLOAT/MUL. Les combinaisons autorisées par le partitionnement INT-FLOAT-MEM étaient donc possibles, en plus des combinaisons INT + INT, voire INT + INT + MUL (vu que la FPU est reconditionnée). Autrement dit, il pouvait faire toute ce que permettait un CPU à double émission entière, plus les combinaisons FLOAT/MUL + MEM.
Le SuperSPARC était un CPU triple émission, avec deux ALU entières, une FPU et une unité mémoire. Il pouvait émettre, à chaque cycle, trois µops en piochant dans deux opérations entières, un accès mémoire, une opération flottante. Le SuperSPARC avait de nombreuses contraintes d’appariement. Par exemple, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Une bonne partie de ces contraintes était lié au banc de registre, qui n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions.
: La seconde ALU a été abandonnée dans l'HyperSPARC, qui est de plus repassé en double émission, ce qui est une évolution à rebours de l'évolution des autres CPUs. Le SuperSPARC était en avance sur son temps, ce quelques années, alors que l'HyperSPARC a un design de son temps.
===Les processeurs historiques à quadruple émission===
C'est la même année , en 1994, que la quadruple émission est arrivée et que les CPU ont commencé à avoir de plus en plus d'unités de calcul. Même les designs restés en double émission avaient ajouté des unités de calcul. Il y avait alors trois stratégies pour la quadruple émission.
La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Les FPUs étaient soit dupliquées, soit scindées en deux en séparant l'additionneur du multiplieur flottant. Le POWER 2 dupliquait ses FPUs à la perfection, alors que l'21264 partitionnait la FPU. Les accès mémoire étaient traités comme des opérations entières, et le processeur pouvait : soit en faire un seul par cycle, soit en émettre deux, sous conditions (le cache devait être double port pour lire/écrire deux données à la fois, comme on le verra dans la suite du chapitre).
Les processeurs MIPS R8000 et R10000 allaient un peu plus loin, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités.
La seconde implémentation dupliquait l'ALU entière et combinait le tout avec un partitionnement INT-FLOAT-MEM. De quoi exécuter en même temps deux opérations entières, une opération flottante, et un accès mémoire. 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).
Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, avec au minimum deux ALU entières. Pour comprendre ce que je veux dire, prenons l'exemple de l'UltraSPARC. Il avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplier flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes. Le processeur pouvait émettre 4 instructions, tant qu'elles atterrissaient dans des unités différentes.
La même méthode peut être utilisée pour la double émission. Le Motorola 88110, sorti en 1991, était un CPU à double émission avec pas moins de 10 unités : deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise.
La morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes. Le tout est résumé dans le tableau ci-dessous, avec :
* en vert les CPU utilisant la première stratégie ;
* en rouge les CPU utilisant la seconde stratégie ;
* en jaune les CPU utilisant la troisième stratégie.
{|class="wikitable"
|+ Processeurs quadruple émission historiques
|-
! Année !! Processeur !! Unités
|-
! 1991
| class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques
|-
|-
! rowspan="2" | 1994
| class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'')
|-
| class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM
|-
! rowspan="3" | 1995
| class="f_jaune | PA 8000 || class="f_jaune | 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD
|-
| class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques
|-
| MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM
|-
! rowspan="2" | 1996
| class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM
|-
| class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL)
|}
===Les processeurs superscalaires modernes===
Les CPU superscalaires modernes ont des design légèrement différents des CPU historiques. Ils dupliquent l'unité entière, mais leur partitionnement est différent. En général, les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Par exemple, un CPU à 6 voies pourra émettre/exécuter 6 opérations entières simultanément, grâce à 6 ALU entières. Mais il s'agit là d'un maximum et certaines peuvent être remplacées par une opération flottante ou mémoire, qui utilisent l'unité mémoire et/ou la FPU.
Un exemple assez parlant est les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon. C'était une microarchitecture à triple émission, avec trois ALU entières et trois unités mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements.
Il s'agit là d'une tendance, cependant. Les CPU superscalaires tendent à mettre le paquet niveau opérations arithmétiques, mais pas forcément au point de remplir la totalité de la largeur du processeur. La règle est que pour N voies, le processeur dispose d'au moins N/2 ALU entières. Attention, cela ne signifie pas que le processeur émet systématiquement N/2 opération entières. Il peut par exemple émettre une combinaison d'opérations flottantes et mémoire, avec des branchements. Et N/2 est une borne basse, la plupart des CPU x86 ont environ tournent plutôt autour des 2/3, voire des 3/4.
En général, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. La conséquence est que les unités mémoire sont dupliquées, en seconde place dans l'ordre d'importance. Les processeurs x86 ont commencé à dupliquer leurs unités mémoire dès qu'ils ont atteint la quadruple émission, pour les CPU grand public. La microarchitecture Core d'Intel utilisait la quadruple émission, avec deux unités mémoire et trois ALU entières. Les microarchitectures d'AMD K5 et K6 utilisaient aussi la quadruple émission et avaient deux ALUs pour deux unités mémoire.
Plus récemment, la microarchitecture ''Golden Cove'' dispose de cinq unités de calcul trois unités mémoire (deux spécialisées dans les écritures, trois dans les lectures). La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire.Il y a donc soit égalité, soit avantage pour les ALU entières.
Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair.
==L'unité d'émission d'un processeur superscalaire==
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Voyons pourquoi.
===L'excès de ports d'émission===
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ou 5 ?
Une solution est de réduire la largeur du processeur. Dans l'exemple précédent, il est possible de passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La solution précédente est certes très intéressante, mais elle a des défauts. Le principal est que les ports d'émission sont sous-utilisés. Et les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
===L'intérieur de l'unité d'émission===
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Le banc de registre d'un processeur superscalaire===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Une autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions auto-aligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions auto-aligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez longs, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
1wgycz6z0aqoz2qm5tj3kc0xa6j8eml
773150
773149
2026-09-25T19:05:52Z
Mewtow
31375
/* La duplication des ALU entières */
773150
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===L'impact des contraintes d’appariement sur la performance===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour la suite du chapitre===
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==Le ''front-end'' des processeurs superscalaires==
Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple.
Intuitivement, charger, décoder et 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 certaines processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. 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
|}
Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs.
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
==Les processeurs implémentés avec plusieurs pipelines==
Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Vous vous dites que l'implémentation d'un processeur superscalaire n'est pas la même suivant que le CPU utilise l'exécution dans le désordre, ou non. Mais dans les faits, la distinction est plus subtile que ca. Pour l'expliquer, je vais utiliser une classification originale, qui sépare les processeurs superscalaires en trois sous-types. La distinction se fait sur la manière dont la superscalarité est implémentée en matériel.La classification que j'utilise distingue :
* Les processeurs avec plusieurs pipelines, sans exécution dans le désordre.
* Les processeurs superscalaires avec un pipeline dynamique.
* Les processeurs avec des fenêtres d'instruction décentralisées.
La distinction n'est pas qu'une question d'implémentation matérielle. Elle impacte aussi le ratio entre instructions chargées/décodées et instructions émises. Vous vous dites que les deux sont égales, mais ce n'est le cas que sur les processeurs avec N pipelines ! Sur les autres processeurs, il est possible de décoder 4 instructions, mais d'en émettre maximum 6 par cycle ! Et sur les CPU avec des fenêtres d'instruction décentralisées, il y a aussi une différence entre le nombre d'instructions qui sont ''dispatch'' et ''schedule''.
La catégorie la plus simple est celle des '''processeurs avec plusieurs pipelines'''. Ce sont des processeurs superscalaires sans exécution dans le désordre. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction.
Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, d'une FPU, éventuellement d'un second banc de registres, guère plus. Il permet d'exécuter une seconde opération de calcul, pas plus, ce qui est à l'origine de contraintes d'appariemment assez fortes. De plus, il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section.
===La double émission entière-flottante===
L'exemple le plus simple utilise un pipeline pour les opérations flottantes, séparé du pipeline originel. Pour bien expliquer les choses, prenons le processeur illustré ci-dessous. Un processeur a deux ''back-end'' séparés, un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par l'ALU entière, passent éventuellement dans l'unité mémoire, pour ensuite arriver à l'étape de ''writeback''. La FPU et le banc de registre flottant ont eux aussi leur propre ''back-end'' séparé.
[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]]
Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaqaue pipeline, à chaque cycle.
L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
===La double émission entière===
Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait.
Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
==Les unités de calcul d'un processeur superscalaire==
Après avoir vu le chargement et le décodage superscalaire, voyons ce qu'il en est au niveau des unités de calcul. Nous allons parler des unités de calcul avant de parler de l'émission, pour une raison simple : l'émission fait l'interface entre unité de calcul et ''front-end'' et on ne peut pas parler de l'émission sans avoir vu comment la superscalarité impacte les deux. Si le ''front-end'' charge et décode un paquet de N instructions, son exécution demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est possible d'exploiter plusieurs unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu.
Les tout premiers CPU superscalaires étaient nombreux à utiliser cette technique. Ils étaient des CPU double émission, avec une ALU entière, une unité mémoire et une FPU. Nous parlerons de '''partitionnement INT-FLOAT-MEM'''. Il s'agit de l'HyperSPARC, de l'alpha 21064 et du PowerPC 603. Les contrainte d’appariement variaient grandement suivant le processeur. Les plus permissifs autorisaient toutes les combinaisons possibles entre : une opération entière, une opération flottante, et une unité mémoire. Mais ils n'étaient pas les seuls.
Le PowerPC 603 implémentait ce genre de double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions).
Il est aussi possible de faire la même chose pour les opérations entières, vu qu'elles sont prises en charge par trois circuits. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres. Il faut parfois lui ajouter le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible d'émettre une opération séparément, ce qui donne le '''partitionnement entier''' dans ce qui suit. Le partitionnement entier n'est jamais utilisé tel quel, car le gain en performance serait trop faible au regard des couts. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Le '''reconditionnement d'une unité fonctionnelle''' permet à celle-ci de faire des opérations qu'elle n'est pas prévue pour. Par exemple, il est possible d'utiliser l'unité mémoire comme seconde ALU entière, ou d'utiliser la FPU comme multiplieur. Ces deux exemples ne sont pas choisit au hasard, nous les étudierons dans ce qui suit. Le reconditionnement évite d'avoir à dupliquer une unité de calcul, tout en ajoutant une voie. Il permet d'améliorer l'efficacité du partitionnement, qu'il soit utilisé seul ou non.
Il est possible de reconditionner la FPU pour faire des multiplications entières. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits. Il est possible d'utiliser de multiplieur pour faire des multiplications entières 32 bits. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
Le reconditionnement de la FPU permet d'exécuter une multiplication entière en plus des autres opérations entières. Par exemple, prenons un CPU avec double émission entière-flottante, sans multiplieur entier séparé. Le reconditionnement de la FPU permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine.
Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants.
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''. Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, 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]]
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications.
Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que dupliquer l'ALU entière peut se marier parfaitement avec la présence d'une FPU. Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU reconditionne sa FPU ou pas. Mais il est très rare qu'un CPU ait à la fois un multiplieur entier et une FPU reconditionnée.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires modernes utilisent toutes ces stratégies===
Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, duplication de l'ALU entière, duplication des autre unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents.
* Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU.
* Le '''reconditionnement des unités''' a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU.
* La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants.
* La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables.
De nos jours, le partitionnement est utilisé en complément de la duplication des ALU entières, et de la duplication des autres unités. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993.
Le meilleur moyen de voir comment sont combinés ces quatre technique est de regarder ce qu'il s'est passé historiquement.
==L'évolution historique de la superscalarité==
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. La plupart des CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 sont rapidement montés à de la quadruple émission, avec quelques exceptions. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 ||
|-
! 1995
| 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000
|-
! 1996
| class="f_bleu | 620 || || || || class="f_bleu | 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
===Les processeurs historiques à double émission===
Pour commencer, étudions le cas des premiers CPU superscalaires à double émission. Ils sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Notamment, la duplication des autres unités ne sert pour ainsi dire à rien, ce qui limite les possibilités, mais rendra les explications plus simples.
Il pouvaient se classer en deux/trois types principaux, assez stricts, avec quelques processeurs qui ne rentraient pas dans ces cases. Le premier utilise le partitionnement seul, le second dupliquer les ALU entières sans partitionnement, et le troisième type est volontairement passé sous silence pour le moment (nous aborderons le troisième type dans la partie sur des design à quadruple émission).
Les CPU n'utilisant que le partitionnement faisaient la double émission avec trois unités : une ALU, une FPU et une unité mémoire. Le reconditionnement de la FPU était parfois utilisé, mais l'AGU n'était jamais reconditionnée. Si la moitié des processeurs faisait du ''partitionnement complet'' entre ALU, FPU et unité mémoire, l'autre moitié utilisait la bouble émission entière-flottante.
Pour résumer, voici le tableau chronologique disant quel CPU utilisait quelle technique.
* Le partitionnement INT-FLOAT-MEM est en jaune.
* La double émission entière-flottante est en vert.
* La double émission entière est en rouge.
* Les CPU barrés utilisent la quadruple émission et seront vu dans la section suivante.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| class="f_vert | RS/6000 || || || class="f_rouge | i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| class="f_vert | RSC || class="f_vert | PA 7100 || SuperSPARC || || class="f_jaune | 21064 ||
|-
! 1993
| class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || ||
|-
! 1994
| <s>POWER 2</s> || PA 7200 || || || <s>21164</s> ||
|}
Vous voyez que certains processeur sont laissés de côté dans le tableau précédent. C'est car ils ne rentrent pas dans les catégories précédentes et faisaient autrement. Si on omet le Motorola 88110, qui utilisait une forme de double émission assez avancée, les deux CPUs qui vont nous intéresser sont le PA-RISC PA 7200 d'HP, et le SuperSPARC.
Le PA 7200, sorti en 1994, est un PA-7100 auquel on aurait rajouté une ALU entière. Précisons que ce processeur utilisait aussi le reconditionnement de la FPU, comme son prédécesseur. Les combinaisons autorisées étaient donc INT + INT, INT + MEM, INT + FLOAT/MUL, MEM + FLOAT/MUL. Les combinaisons autorisées par le partitionnement INT-FLOAT-MEM étaient donc possibles, en plus des combinaisons INT + INT, voire INT + INT + MUL (vu que la FPU est reconditionnée). Autrement dit, il pouvait faire toute ce que permettait un CPU à double émission entière, plus les combinaisons FLOAT/MUL + MEM.
Le SuperSPARC était un CPU triple émission, avec deux ALU entières, une FPU et une unité mémoire. Il pouvait émettre, à chaque cycle, trois µops en piochant dans deux opérations entières, un accès mémoire, une opération flottante. Le SuperSPARC avait de nombreuses contraintes d’appariement. Par exemple, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Une bonne partie de ces contraintes était lié au banc de registre, qui n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions.
: La seconde ALU a été abandonnée dans l'HyperSPARC, qui est de plus repassé en double émission, ce qui est une évolution à rebours de l'évolution des autres CPUs. Le SuperSPARC était en avance sur son temps, ce quelques années, alors que l'HyperSPARC a un design de son temps.
===Les processeurs historiques à quadruple émission===
C'est la même année , en 1994, que la quadruple émission est arrivée et que les CPU ont commencé à avoir de plus en plus d'unités de calcul. Même les designs restés en double émission avaient ajouté des unités de calcul. Il y avait alors trois stratégies pour la quadruple émission.
La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Les FPUs étaient soit dupliquées, soit scindées en deux en séparant l'additionneur du multiplieur flottant. Le POWER 2 dupliquait ses FPUs à la perfection, alors que l'21264 partitionnait la FPU. Les accès mémoire étaient traités comme des opérations entières, et le processeur pouvait : soit en faire un seul par cycle, soit en émettre deux, sous conditions (le cache devait être double port pour lire/écrire deux données à la fois, comme on le verra dans la suite du chapitre).
Les processeurs MIPS R8000 et R10000 allaient un peu plus loin, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités.
La seconde implémentation dupliquait l'ALU entière et combinait le tout avec un partitionnement INT-FLOAT-MEM. De quoi exécuter en même temps deux opérations entières, une opération flottante, et un accès mémoire. 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).
Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, avec au minimum deux ALU entières. Pour comprendre ce que je veux dire, prenons l'exemple de l'UltraSPARC. Il avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplier flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes. Le processeur pouvait émettre 4 instructions, tant qu'elles atterrissaient dans des unités différentes.
La même méthode peut être utilisée pour la double émission. Le Motorola 88110, sorti en 1991, était un CPU à double émission avec pas moins de 10 unités : deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise.
La morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes. Le tout est résumé dans le tableau ci-dessous, avec :
* en vert les CPU utilisant la première stratégie ;
* en rouge les CPU utilisant la seconde stratégie ;
* en jaune les CPU utilisant la troisième stratégie.
{|class="wikitable"
|+ Processeurs quadruple émission historiques
|-
! Année !! Processeur !! Unités
|-
! 1991
| class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques
|-
|-
! rowspan="2" | 1994
| class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'')
|-
| class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM
|-
! rowspan="3" | 1995
| class="f_jaune | PA 8000 || class="f_jaune | 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD
|-
| class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques
|-
| MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM
|-
! rowspan="2" | 1996
| class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM
|-
| class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL)
|}
===Les processeurs superscalaires modernes===
Les CPU superscalaires modernes ont des design légèrement différents des CPU historiques. Ils dupliquent l'unité entière, mais leur partitionnement est différent. En général, les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Par exemple, un CPU à 6 voies pourra émettre/exécuter 6 opérations entières simultanément, grâce à 6 ALU entières. Mais il s'agit là d'un maximum et certaines peuvent être remplacées par une opération flottante ou mémoire, qui utilisent l'unité mémoire et/ou la FPU.
Un exemple assez parlant est les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon. C'était une microarchitecture à triple émission, avec trois ALU entières et trois unités mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements.
Il s'agit là d'une tendance, cependant. Les CPU superscalaires tendent à mettre le paquet niveau opérations arithmétiques, mais pas forcément au point de remplir la totalité de la largeur du processeur. La règle est que pour N voies, le processeur dispose d'au moins N/2 ALU entières. Attention, cela ne signifie pas que le processeur émet systématiquement N/2 opération entières. Il peut par exemple émettre une combinaison d'opérations flottantes et mémoire, avec des branchements. Et N/2 est une borne basse, la plupart des CPU x86 ont environ tournent plutôt autour des 2/3, voire des 3/4.
En général, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. La conséquence est que les unités mémoire sont dupliquées, en seconde place dans l'ordre d'importance. Les processeurs x86 ont commencé à dupliquer leurs unités mémoire dès qu'ils ont atteint la quadruple émission, pour les CPU grand public. La microarchitecture Core d'Intel utilisait la quadruple émission, avec deux unités mémoire et trois ALU entières. Les microarchitectures d'AMD K5 et K6 utilisaient aussi la quadruple émission et avaient deux ALUs pour deux unités mémoire.
Plus récemment, la microarchitecture ''Golden Cove'' dispose de cinq unités de calcul trois unités mémoire (deux spécialisées dans les écritures, trois dans les lectures). La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire.Il y a donc soit égalité, soit avantage pour les ALU entières.
Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair.
==L'unité d'émission d'un processeur superscalaire==
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Voyons pourquoi.
===L'excès de ports d'émission===
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ou 5 ?
Une solution est de réduire la largeur du processeur. Dans l'exemple précédent, il est possible de passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La solution précédente est certes très intéressante, mais elle a des défauts. Le principal est que les ports d'émission sont sous-utilisés. Et les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
===L'intérieur de l'unité d'émission===
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Le banc de registre d'un processeur superscalaire===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Une autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions auto-aligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions auto-aligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez longs, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
4dr05yiwl67kpz7zp582lqezqsc43ae
773151
773150
2026-09-25T19:07:03Z
Mewtow
31375
/* La duplication des ALU entières */
773151
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===L'impact des contraintes d’appariement sur la performance===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour la suite du chapitre===
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==Le ''front-end'' des processeurs superscalaires==
Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple.
Intuitivement, charger, décoder et 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 certaines processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. 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
|}
Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs.
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
==Les processeurs implémentés avec plusieurs pipelines==
Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Vous vous dites que l'implémentation d'un processeur superscalaire n'est pas la même suivant que le CPU utilise l'exécution dans le désordre, ou non. Mais dans les faits, la distinction est plus subtile que ca. Pour l'expliquer, je vais utiliser une classification originale, qui sépare les processeurs superscalaires en trois sous-types. La distinction se fait sur la manière dont la superscalarité est implémentée en matériel.La classification que j'utilise distingue :
* Les processeurs avec plusieurs pipelines, sans exécution dans le désordre.
* Les processeurs superscalaires avec un pipeline dynamique.
* Les processeurs avec des fenêtres d'instruction décentralisées.
La distinction n'est pas qu'une question d'implémentation matérielle. Elle impacte aussi le ratio entre instructions chargées/décodées et instructions émises. Vous vous dites que les deux sont égales, mais ce n'est le cas que sur les processeurs avec N pipelines ! Sur les autres processeurs, il est possible de décoder 4 instructions, mais d'en émettre maximum 6 par cycle ! Et sur les CPU avec des fenêtres d'instruction décentralisées, il y a aussi une différence entre le nombre d'instructions qui sont ''dispatch'' et ''schedule''.
La catégorie la plus simple est celle des '''processeurs avec plusieurs pipelines'''. Ce sont des processeurs superscalaires sans exécution dans le désordre. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction.
Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, d'une FPU, éventuellement d'un second banc de registres, guère plus. Il permet d'exécuter une seconde opération de calcul, pas plus, ce qui est à l'origine de contraintes d'appariemment assez fortes. De plus, il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section.
===La double émission entière-flottante===
L'exemple le plus simple utilise un pipeline pour les opérations flottantes, séparé du pipeline originel. Pour bien expliquer les choses, prenons le processeur illustré ci-dessous. Un processeur a deux ''back-end'' séparés, un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par l'ALU entière, passent éventuellement dans l'unité mémoire, pour ensuite arriver à l'étape de ''writeback''. La FPU et le banc de registre flottant ont eux aussi leur propre ''back-end'' séparé.
[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]]
Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaqaue pipeline, à chaque cycle.
L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
===La double émission entière===
Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait.
Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
==Les unités de calcul d'un processeur superscalaire==
Après avoir vu le chargement et le décodage superscalaire, voyons ce qu'il en est au niveau des unités de calcul. Nous allons parler des unités de calcul avant de parler de l'émission, pour une raison simple : l'émission fait l'interface entre unité de calcul et ''front-end'' et on ne peut pas parler de l'émission sans avoir vu comment la superscalarité impacte les deux. Si le ''front-end'' charge et décode un paquet de N instructions, son exécution demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est possible d'exploiter plusieurs unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu.
Les tout premiers CPU superscalaires étaient nombreux à utiliser cette technique. Ils étaient des CPU double émission, avec une ALU entière, une unité mémoire et une FPU. Nous parlerons de '''partitionnement INT-FLOAT-MEM'''. Il s'agit de l'HyperSPARC, de l'alpha 21064 et du PowerPC 603. Les contrainte d’appariement variaient grandement suivant le processeur. Les plus permissifs autorisaient toutes les combinaisons possibles entre : une opération entière, une opération flottante, et une unité mémoire. Mais ils n'étaient pas les seuls.
Le PowerPC 603 implémentait ce genre de double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions).
Il est aussi possible de faire la même chose pour les opérations entières, vu qu'elles sont prises en charge par trois circuits. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres. Il faut parfois lui ajouter le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible d'émettre une opération séparément, ce qui donne le '''partitionnement entier''' dans ce qui suit. Le partitionnement entier n'est jamais utilisé tel quel, car le gain en performance serait trop faible au regard des couts. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Le '''reconditionnement d'une unité fonctionnelle''' permet à celle-ci de faire des opérations qu'elle n'est pas prévue pour. Par exemple, il est possible d'utiliser l'unité mémoire comme seconde ALU entière, ou d'utiliser la FPU comme multiplieur. Ces deux exemples ne sont pas choisit au hasard, nous les étudierons dans ce qui suit. Le reconditionnement évite d'avoir à dupliquer une unité de calcul, tout en ajoutant une voie. Il permet d'améliorer l'efficacité du partitionnement, qu'il soit utilisé seul ou non.
Il est possible de reconditionner la FPU pour faire des multiplications entières. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits. Il est possible d'utiliser de multiplieur pour faire des multiplications entières 32 bits. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
Le reconditionnement de la FPU permet d'exécuter une multiplication entière en plus des autres opérations entières. Par exemple, prenons un CPU avec double émission entière-flottante, sans multiplieur entier séparé. Le reconditionnement de la FPU permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine.
Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants.
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''. Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, 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]]
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications.
Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires modernes utilisent toutes ces stratégies===
Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, duplication de l'ALU entière, duplication des autre unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents.
* Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU.
* Le '''reconditionnement des unités''' a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU.
* La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants.
* La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables.
De nos jours, le partitionnement est utilisé en complément de la duplication des ALU entières, et de la duplication des autres unités. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993.
Le meilleur moyen de voir comment sont combinés ces quatre technique est de regarder ce qu'il s'est passé historiquement.
==L'évolution historique de la superscalarité==
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. La plupart des CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 sont rapidement montés à de la quadruple émission, avec quelques exceptions. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 ||
|-
! 1995
| 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000
|-
! 1996
| class="f_bleu | 620 || || || || class="f_bleu | 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
===Les processeurs historiques à double émission===
Pour commencer, étudions le cas des premiers CPU superscalaires à double émission. Ils sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Notamment, la duplication des autres unités ne sert pour ainsi dire à rien, ce qui limite les possibilités, mais rendra les explications plus simples.
Il pouvaient se classer en deux/trois types principaux, assez stricts, avec quelques processeurs qui ne rentraient pas dans ces cases. Le premier utilise le partitionnement seul, le second dupliquer les ALU entières sans partitionnement, et le troisième type est volontairement passé sous silence pour le moment (nous aborderons le troisième type dans la partie sur des design à quadruple émission).
Les CPU n'utilisant que le partitionnement faisaient la double émission avec trois unités : une ALU, une FPU et une unité mémoire. Le reconditionnement de la FPU était parfois utilisé, mais l'AGU n'était jamais reconditionnée. Si la moitié des processeurs faisait du ''partitionnement complet'' entre ALU, FPU et unité mémoire, l'autre moitié utilisait la bouble émission entière-flottante.
Pour résumer, voici le tableau chronologique disant quel CPU utilisait quelle technique.
* Le partitionnement INT-FLOAT-MEM est en jaune.
* La double émission entière-flottante est en vert.
* La double émission entière est en rouge.
* Les CPU barrés utilisent la quadruple émission et seront vu dans la section suivante.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| class="f_vert | RS/6000 || || || class="f_rouge | i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| class="f_vert | RSC || class="f_vert | PA 7100 || SuperSPARC || || class="f_jaune | 21064 ||
|-
! 1993
| class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || ||
|-
! 1994
| <s>POWER 2</s> || PA 7200 || || || <s>21164</s> ||
|}
Vous voyez que certains processeur sont laissés de côté dans le tableau précédent. C'est car ils ne rentrent pas dans les catégories précédentes et faisaient autrement. Si on omet le Motorola 88110, qui utilisait une forme de double émission assez avancée, les deux CPUs qui vont nous intéresser sont le PA-RISC PA 7200 d'HP, et le SuperSPARC.
Le PA 7200, sorti en 1994, est un PA-7100 auquel on aurait rajouté une ALU entière. Précisons que ce processeur utilisait aussi le reconditionnement de la FPU, comme son prédécesseur. Les combinaisons autorisées étaient donc INT + INT, INT + MEM, INT + FLOAT/MUL, MEM + FLOAT/MUL. Les combinaisons autorisées par le partitionnement INT-FLOAT-MEM étaient donc possibles, en plus des combinaisons INT + INT, voire INT + INT + MUL (vu que la FPU est reconditionnée). Autrement dit, il pouvait faire toute ce que permettait un CPU à double émission entière, plus les combinaisons FLOAT/MUL + MEM.
Le SuperSPARC était un CPU triple émission, avec deux ALU entières, une FPU et une unité mémoire. Il pouvait émettre, à chaque cycle, trois µops en piochant dans deux opérations entières, un accès mémoire, une opération flottante. Le SuperSPARC avait de nombreuses contraintes d’appariement. Par exemple, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Une bonne partie de ces contraintes était lié au banc de registre, qui n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions.
: La seconde ALU a été abandonnée dans l'HyperSPARC, qui est de plus repassé en double émission, ce qui est une évolution à rebours de l'évolution des autres CPUs. Le SuperSPARC était en avance sur son temps, ce quelques années, alors que l'HyperSPARC a un design de son temps.
===Les processeurs historiques à quadruple émission===
C'est la même année , en 1994, que la quadruple émission est arrivée et que les CPU ont commencé à avoir de plus en plus d'unités de calcul. Même les designs restés en double émission avaient ajouté des unités de calcul. Il y avait alors trois stratégies pour la quadruple émission.
La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Les FPUs étaient soit dupliquées, soit scindées en deux en séparant l'additionneur du multiplieur flottant. Le POWER 2 dupliquait ses FPUs à la perfection, alors que l'21264 partitionnait la FPU. Les accès mémoire étaient traités comme des opérations entières, et le processeur pouvait : soit en faire un seul par cycle, soit en émettre deux, sous conditions (le cache devait être double port pour lire/écrire deux données à la fois, comme on le verra dans la suite du chapitre).
Les processeurs MIPS R8000 et R10000 allaient un peu plus loin, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités.
La seconde implémentation dupliquait l'ALU entière et combinait le tout avec un partitionnement INT-FLOAT-MEM. De quoi exécuter en même temps deux opérations entières, une opération flottante, et un accès mémoire. 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).
Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, avec au minimum deux ALU entières. Pour comprendre ce que je veux dire, prenons l'exemple de l'UltraSPARC. Il avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplier flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes. Le processeur pouvait émettre 4 instructions, tant qu'elles atterrissaient dans des unités différentes.
La même méthode peut être utilisée pour la double émission. Le Motorola 88110, sorti en 1991, était un CPU à double émission avec pas moins de 10 unités : deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise.
La morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes. Le tout est résumé dans le tableau ci-dessous, avec :
* en vert les CPU utilisant la première stratégie ;
* en rouge les CPU utilisant la seconde stratégie ;
* en jaune les CPU utilisant la troisième stratégie.
{|class="wikitable"
|+ Processeurs quadruple émission historiques
|-
! Année !! Processeur !! Unités
|-
! 1991
| class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques
|-
|-
! rowspan="2" | 1994
| class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'')
|-
| class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM
|-
! rowspan="3" | 1995
| class="f_jaune | PA 8000 || class="f_jaune | 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD
|-
| class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques
|-
| MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM
|-
! rowspan="2" | 1996
| class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM
|-
| class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL)
|}
===Les processeurs superscalaires modernes===
Les CPU superscalaires modernes ont des design légèrement différents des CPU historiques. Ils dupliquent l'unité entière, mais leur partitionnement est différent. En général, les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Par exemple, un CPU à 6 voies pourra émettre/exécuter 6 opérations entières simultanément, grâce à 6 ALU entières. Mais il s'agit là d'un maximum et certaines peuvent être remplacées par une opération flottante ou mémoire, qui utilisent l'unité mémoire et/ou la FPU.
Un exemple assez parlant est les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon. C'était une microarchitecture à triple émission, avec trois ALU entières et trois unités mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements.
Il s'agit là d'une tendance, cependant. Les CPU superscalaires tendent à mettre le paquet niveau opérations arithmétiques, mais pas forcément au point de remplir la totalité de la largeur du processeur. La règle est que pour N voies, le processeur dispose d'au moins N/2 ALU entières. Attention, cela ne signifie pas que le processeur émet systématiquement N/2 opération entières. Il peut par exemple émettre une combinaison d'opérations flottantes et mémoire, avec des branchements. Et N/2 est une borne basse, la plupart des CPU x86 ont environ tournent plutôt autour des 2/3, voire des 3/4.
En général, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. La conséquence est que les unités mémoire sont dupliquées, en seconde place dans l'ordre d'importance. Les processeurs x86 ont commencé à dupliquer leurs unités mémoire dès qu'ils ont atteint la quadruple émission, pour les CPU grand public. La microarchitecture Core d'Intel utilisait la quadruple émission, avec deux unités mémoire et trois ALU entières. Les microarchitectures d'AMD K5 et K6 utilisaient aussi la quadruple émission et avaient deux ALUs pour deux unités mémoire.
Plus récemment, la microarchitecture ''Golden Cove'' dispose de cinq unités de calcul trois unités mémoire (deux spécialisées dans les écritures, trois dans les lectures). La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire.Il y a donc soit égalité, soit avantage pour les ALU entières.
Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair.
==L'unité d'émission d'un processeur superscalaire==
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Voyons pourquoi.
===L'excès de ports d'émission===
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ou 5 ?
Une solution est de réduire la largeur du processeur. Dans l'exemple précédent, il est possible de passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La solution précédente est certes très intéressante, mais elle a des défauts. Le principal est que les ports d'émission sont sous-utilisés. Et les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
===L'intérieur de l'unité d'émission===
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Le banc de registre d'un processeur superscalaire===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Une autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions auto-aligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions auto-aligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez longs, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
oe0vammcvck7gbnuqosi2jc6q80rad4
773152
773151
2026-09-25T19:07:26Z
Mewtow
31375
/* Les CPU superscalaires modernes utilisent toutes ces stratégies */
773152
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===L'impact des contraintes d’appariement sur la performance===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour la suite du chapitre===
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==Le ''front-end'' des processeurs superscalaires==
Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple.
Intuitivement, charger, décoder et 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 certaines processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. 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
|}
Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs.
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
==Les processeurs implémentés avec plusieurs pipelines==
Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Vous vous dites que l'implémentation d'un processeur superscalaire n'est pas la même suivant que le CPU utilise l'exécution dans le désordre, ou non. Mais dans les faits, la distinction est plus subtile que ca. Pour l'expliquer, je vais utiliser une classification originale, qui sépare les processeurs superscalaires en trois sous-types. La distinction se fait sur la manière dont la superscalarité est implémentée en matériel.La classification que j'utilise distingue :
* Les processeurs avec plusieurs pipelines, sans exécution dans le désordre.
* Les processeurs superscalaires avec un pipeline dynamique.
* Les processeurs avec des fenêtres d'instruction décentralisées.
La distinction n'est pas qu'une question d'implémentation matérielle. Elle impacte aussi le ratio entre instructions chargées/décodées et instructions émises. Vous vous dites que les deux sont égales, mais ce n'est le cas que sur les processeurs avec N pipelines ! Sur les autres processeurs, il est possible de décoder 4 instructions, mais d'en émettre maximum 6 par cycle ! Et sur les CPU avec des fenêtres d'instruction décentralisées, il y a aussi une différence entre le nombre d'instructions qui sont ''dispatch'' et ''schedule''.
La catégorie la plus simple est celle des '''processeurs avec plusieurs pipelines'''. Ce sont des processeurs superscalaires sans exécution dans le désordre. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction.
Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, d'une FPU, éventuellement d'un second banc de registres, guère plus. Il permet d'exécuter une seconde opération de calcul, pas plus, ce qui est à l'origine de contraintes d'appariemment assez fortes. De plus, il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section.
===La double émission entière-flottante===
L'exemple le plus simple utilise un pipeline pour les opérations flottantes, séparé du pipeline originel. Pour bien expliquer les choses, prenons le processeur illustré ci-dessous. Un processeur a deux ''back-end'' séparés, un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par l'ALU entière, passent éventuellement dans l'unité mémoire, pour ensuite arriver à l'étape de ''writeback''. La FPU et le banc de registre flottant ont eux aussi leur propre ''back-end'' séparé.
[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]]
Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaqaue pipeline, à chaque cycle.
L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
===La double émission entière===
Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait.
Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
==Les unités de calcul d'un processeur superscalaire==
Après avoir vu le chargement et le décodage superscalaire, voyons ce qu'il en est au niveau des unités de calcul. Nous allons parler des unités de calcul avant de parler de l'émission, pour une raison simple : l'émission fait l'interface entre unité de calcul et ''front-end'' et on ne peut pas parler de l'émission sans avoir vu comment la superscalarité impacte les deux. Si le ''front-end'' charge et décode un paquet de N instructions, son exécution demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est possible d'exploiter plusieurs unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu.
Les tout premiers CPU superscalaires étaient nombreux à utiliser cette technique. Ils étaient des CPU double émission, avec une ALU entière, une unité mémoire et une FPU. Nous parlerons de '''partitionnement INT-FLOAT-MEM'''. Il s'agit de l'HyperSPARC, de l'alpha 21064 et du PowerPC 603. Les contrainte d’appariement variaient grandement suivant le processeur. Les plus permissifs autorisaient toutes les combinaisons possibles entre : une opération entière, une opération flottante, et une unité mémoire. Mais ils n'étaient pas les seuls.
Le PowerPC 603 implémentait ce genre de double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions).
Il est aussi possible de faire la même chose pour les opérations entières, vu qu'elles sont prises en charge par trois circuits. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres. Il faut parfois lui ajouter le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible d'émettre une opération séparément, ce qui donne le '''partitionnement entier''' dans ce qui suit. Le partitionnement entier n'est jamais utilisé tel quel, car le gain en performance serait trop faible au regard des couts. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Le '''reconditionnement d'une unité fonctionnelle''' permet à celle-ci de faire des opérations qu'elle n'est pas prévue pour. Par exemple, il est possible d'utiliser l'unité mémoire comme seconde ALU entière, ou d'utiliser la FPU comme multiplieur. Ces deux exemples ne sont pas choisit au hasard, nous les étudierons dans ce qui suit. Le reconditionnement évite d'avoir à dupliquer une unité de calcul, tout en ajoutant une voie. Il permet d'améliorer l'efficacité du partitionnement, qu'il soit utilisé seul ou non.
Il est possible de reconditionner la FPU pour faire des multiplications entières. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits. Il est possible d'utiliser de multiplieur pour faire des multiplications entières 32 bits. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
Le reconditionnement de la FPU permet d'exécuter une multiplication entière en plus des autres opérations entières. Par exemple, prenons un CPU avec double émission entière-flottante, sans multiplieur entier séparé. Le reconditionnement de la FPU permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine.
Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants.
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''. Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, 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]]
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications.
Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires modernes utilisent toutes ces stratégies===
Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autre unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents.
* Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU.
* La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants.
* La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables.
De nos jours, le partitionnement est utilisé en complément de la duplication des ALU entières, et de la duplication des autres unités. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993.
Le meilleur moyen de voir comment sont combinés ces quatre technique est de regarder ce qu'il s'est passé historiquement.
==L'évolution historique de la superscalarité==
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. La plupart des CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 sont rapidement montés à de la quadruple émission, avec quelques exceptions. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 ||
|-
! 1995
| 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000
|-
! 1996
| class="f_bleu | 620 || || || || class="f_bleu | 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
===Les processeurs historiques à double émission===
Pour commencer, étudions le cas des premiers CPU superscalaires à double émission. Ils sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Notamment, la duplication des autres unités ne sert pour ainsi dire à rien, ce qui limite les possibilités, mais rendra les explications plus simples.
Il pouvaient se classer en deux/trois types principaux, assez stricts, avec quelques processeurs qui ne rentraient pas dans ces cases. Le premier utilise le partitionnement seul, le second dupliquer les ALU entières sans partitionnement, et le troisième type est volontairement passé sous silence pour le moment (nous aborderons le troisième type dans la partie sur des design à quadruple émission).
Les CPU n'utilisant que le partitionnement faisaient la double émission avec trois unités : une ALU, une FPU et une unité mémoire. Le reconditionnement de la FPU était parfois utilisé, mais l'AGU n'était jamais reconditionnée. Si la moitié des processeurs faisait du ''partitionnement complet'' entre ALU, FPU et unité mémoire, l'autre moitié utilisait la bouble émission entière-flottante.
Pour résumer, voici le tableau chronologique disant quel CPU utilisait quelle technique.
* Le partitionnement INT-FLOAT-MEM est en jaune.
* La double émission entière-flottante est en vert.
* La double émission entière est en rouge.
* Les CPU barrés utilisent la quadruple émission et seront vu dans la section suivante.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| class="f_vert | RS/6000 || || || class="f_rouge | i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| class="f_vert | RSC || class="f_vert | PA 7100 || SuperSPARC || || class="f_jaune | 21064 ||
|-
! 1993
| class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || ||
|-
! 1994
| <s>POWER 2</s> || PA 7200 || || || <s>21164</s> ||
|}
Vous voyez que certains processeur sont laissés de côté dans le tableau précédent. C'est car ils ne rentrent pas dans les catégories précédentes et faisaient autrement. Si on omet le Motorola 88110, qui utilisait une forme de double émission assez avancée, les deux CPUs qui vont nous intéresser sont le PA-RISC PA 7200 d'HP, et le SuperSPARC.
Le PA 7200, sorti en 1994, est un PA-7100 auquel on aurait rajouté une ALU entière. Précisons que ce processeur utilisait aussi le reconditionnement de la FPU, comme son prédécesseur. Les combinaisons autorisées étaient donc INT + INT, INT + MEM, INT + FLOAT/MUL, MEM + FLOAT/MUL. Les combinaisons autorisées par le partitionnement INT-FLOAT-MEM étaient donc possibles, en plus des combinaisons INT + INT, voire INT + INT + MUL (vu que la FPU est reconditionnée). Autrement dit, il pouvait faire toute ce que permettait un CPU à double émission entière, plus les combinaisons FLOAT/MUL + MEM.
Le SuperSPARC était un CPU triple émission, avec deux ALU entières, une FPU et une unité mémoire. Il pouvait émettre, à chaque cycle, trois µops en piochant dans deux opérations entières, un accès mémoire, une opération flottante. Le SuperSPARC avait de nombreuses contraintes d’appariement. Par exemple, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Une bonne partie de ces contraintes était lié au banc de registre, qui n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions.
: La seconde ALU a été abandonnée dans l'HyperSPARC, qui est de plus repassé en double émission, ce qui est une évolution à rebours de l'évolution des autres CPUs. Le SuperSPARC était en avance sur son temps, ce quelques années, alors que l'HyperSPARC a un design de son temps.
===Les processeurs historiques à quadruple émission===
C'est la même année , en 1994, que la quadruple émission est arrivée et que les CPU ont commencé à avoir de plus en plus d'unités de calcul. Même les designs restés en double émission avaient ajouté des unités de calcul. Il y avait alors trois stratégies pour la quadruple émission.
La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Les FPUs étaient soit dupliquées, soit scindées en deux en séparant l'additionneur du multiplieur flottant. Le POWER 2 dupliquait ses FPUs à la perfection, alors que l'21264 partitionnait la FPU. Les accès mémoire étaient traités comme des opérations entières, et le processeur pouvait : soit en faire un seul par cycle, soit en émettre deux, sous conditions (le cache devait être double port pour lire/écrire deux données à la fois, comme on le verra dans la suite du chapitre).
Les processeurs MIPS R8000 et R10000 allaient un peu plus loin, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités.
La seconde implémentation dupliquait l'ALU entière et combinait le tout avec un partitionnement INT-FLOAT-MEM. De quoi exécuter en même temps deux opérations entières, une opération flottante, et un accès mémoire. 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).
Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, avec au minimum deux ALU entières. Pour comprendre ce que je veux dire, prenons l'exemple de l'UltraSPARC. Il avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplier flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes. Le processeur pouvait émettre 4 instructions, tant qu'elles atterrissaient dans des unités différentes.
La même méthode peut être utilisée pour la double émission. Le Motorola 88110, sorti en 1991, était un CPU à double émission avec pas moins de 10 unités : deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise.
La morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes. Le tout est résumé dans le tableau ci-dessous, avec :
* en vert les CPU utilisant la première stratégie ;
* en rouge les CPU utilisant la seconde stratégie ;
* en jaune les CPU utilisant la troisième stratégie.
{|class="wikitable"
|+ Processeurs quadruple émission historiques
|-
! Année !! Processeur !! Unités
|-
! 1991
| class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques
|-
|-
! rowspan="2" | 1994
| class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'')
|-
| class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM
|-
! rowspan="3" | 1995
| class="f_jaune | PA 8000 || class="f_jaune | 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD
|-
| class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques
|-
| MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM
|-
! rowspan="2" | 1996
| class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM
|-
| class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL)
|}
===Les processeurs superscalaires modernes===
Les CPU superscalaires modernes ont des design légèrement différents des CPU historiques. Ils dupliquent l'unité entière, mais leur partitionnement est différent. En général, les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Par exemple, un CPU à 6 voies pourra émettre/exécuter 6 opérations entières simultanément, grâce à 6 ALU entières. Mais il s'agit là d'un maximum et certaines peuvent être remplacées par une opération flottante ou mémoire, qui utilisent l'unité mémoire et/ou la FPU.
Un exemple assez parlant est les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon. C'était une microarchitecture à triple émission, avec trois ALU entières et trois unités mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements.
Il s'agit là d'une tendance, cependant. Les CPU superscalaires tendent à mettre le paquet niveau opérations arithmétiques, mais pas forcément au point de remplir la totalité de la largeur du processeur. La règle est que pour N voies, le processeur dispose d'au moins N/2 ALU entières. Attention, cela ne signifie pas que le processeur émet systématiquement N/2 opération entières. Il peut par exemple émettre une combinaison d'opérations flottantes et mémoire, avec des branchements. Et N/2 est une borne basse, la plupart des CPU x86 ont environ tournent plutôt autour des 2/3, voire des 3/4.
En général, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. La conséquence est que les unités mémoire sont dupliquées, en seconde place dans l'ordre d'importance. Les processeurs x86 ont commencé à dupliquer leurs unités mémoire dès qu'ils ont atteint la quadruple émission, pour les CPU grand public. La microarchitecture Core d'Intel utilisait la quadruple émission, avec deux unités mémoire et trois ALU entières. Les microarchitectures d'AMD K5 et K6 utilisaient aussi la quadruple émission et avaient deux ALUs pour deux unités mémoire.
Plus récemment, la microarchitecture ''Golden Cove'' dispose de cinq unités de calcul trois unités mémoire (deux spécialisées dans les écritures, trois dans les lectures). La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire.Il y a donc soit égalité, soit avantage pour les ALU entières.
Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair.
==L'unité d'émission d'un processeur superscalaire==
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Voyons pourquoi.
===L'excès de ports d'émission===
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ou 5 ?
Une solution est de réduire la largeur du processeur. Dans l'exemple précédent, il est possible de passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La solution précédente est certes très intéressante, mais elle a des défauts. Le principal est que les ports d'émission sont sous-utilisés. Et les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
===L'intérieur de l'unité d'émission===
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Le banc de registre d'un processeur superscalaire===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Une autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions auto-aligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions auto-aligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez longs, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
a04n0x39gmp3hkqb4g0l4083gjtgx8g
773153
773152
2026-09-25T19:07:55Z
Mewtow
31375
/* Les processeurs historiques à double émission */
773153
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===L'impact des contraintes d’appariement sur la performance===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour la suite du chapitre===
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==Le ''front-end'' des processeurs superscalaires==
Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple.
Intuitivement, charger, décoder et 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 certaines processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. 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
|}
Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs.
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
==Les processeurs implémentés avec plusieurs pipelines==
Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Vous vous dites que l'implémentation d'un processeur superscalaire n'est pas la même suivant que le CPU utilise l'exécution dans le désordre, ou non. Mais dans les faits, la distinction est plus subtile que ca. Pour l'expliquer, je vais utiliser une classification originale, qui sépare les processeurs superscalaires en trois sous-types. La distinction se fait sur la manière dont la superscalarité est implémentée en matériel.La classification que j'utilise distingue :
* Les processeurs avec plusieurs pipelines, sans exécution dans le désordre.
* Les processeurs superscalaires avec un pipeline dynamique.
* Les processeurs avec des fenêtres d'instruction décentralisées.
La distinction n'est pas qu'une question d'implémentation matérielle. Elle impacte aussi le ratio entre instructions chargées/décodées et instructions émises. Vous vous dites que les deux sont égales, mais ce n'est le cas que sur les processeurs avec N pipelines ! Sur les autres processeurs, il est possible de décoder 4 instructions, mais d'en émettre maximum 6 par cycle ! Et sur les CPU avec des fenêtres d'instruction décentralisées, il y a aussi une différence entre le nombre d'instructions qui sont ''dispatch'' et ''schedule''.
La catégorie la plus simple est celle des '''processeurs avec plusieurs pipelines'''. Ce sont des processeurs superscalaires sans exécution dans le désordre. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction.
Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, d'une FPU, éventuellement d'un second banc de registres, guère plus. Il permet d'exécuter une seconde opération de calcul, pas plus, ce qui est à l'origine de contraintes d'appariemment assez fortes. De plus, il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section.
===La double émission entière-flottante===
L'exemple le plus simple utilise un pipeline pour les opérations flottantes, séparé du pipeline originel. Pour bien expliquer les choses, prenons le processeur illustré ci-dessous. Un processeur a deux ''back-end'' séparés, un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par l'ALU entière, passent éventuellement dans l'unité mémoire, pour ensuite arriver à l'étape de ''writeback''. La FPU et le banc de registre flottant ont eux aussi leur propre ''back-end'' séparé.
[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]]
Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaqaue pipeline, à chaque cycle.
L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
===La double émission entière===
Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait.
Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
==Les unités de calcul d'un processeur superscalaire==
Après avoir vu le chargement et le décodage superscalaire, voyons ce qu'il en est au niveau des unités de calcul. Nous allons parler des unités de calcul avant de parler de l'émission, pour une raison simple : l'émission fait l'interface entre unité de calcul et ''front-end'' et on ne peut pas parler de l'émission sans avoir vu comment la superscalarité impacte les deux. Si le ''front-end'' charge et décode un paquet de N instructions, son exécution demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est possible d'exploiter plusieurs unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu.
Les tout premiers CPU superscalaires étaient nombreux à utiliser cette technique. Ils étaient des CPU double émission, avec une ALU entière, une unité mémoire et une FPU. Nous parlerons de '''partitionnement INT-FLOAT-MEM'''. Il s'agit de l'HyperSPARC, de l'alpha 21064 et du PowerPC 603. Les contrainte d’appariement variaient grandement suivant le processeur. Les plus permissifs autorisaient toutes les combinaisons possibles entre : une opération entière, une opération flottante, et une unité mémoire. Mais ils n'étaient pas les seuls.
Le PowerPC 603 implémentait ce genre de double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions).
Il est aussi possible de faire la même chose pour les opérations entières, vu qu'elles sont prises en charge par trois circuits. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres. Il faut parfois lui ajouter le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible d'émettre une opération séparément, ce qui donne le '''partitionnement entier''' dans ce qui suit. Le partitionnement entier n'est jamais utilisé tel quel, car le gain en performance serait trop faible au regard des couts. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Le '''reconditionnement d'une unité fonctionnelle''' permet à celle-ci de faire des opérations qu'elle n'est pas prévue pour. Par exemple, il est possible d'utiliser l'unité mémoire comme seconde ALU entière, ou d'utiliser la FPU comme multiplieur. Ces deux exemples ne sont pas choisit au hasard, nous les étudierons dans ce qui suit. Le reconditionnement évite d'avoir à dupliquer une unité de calcul, tout en ajoutant une voie. Il permet d'améliorer l'efficacité du partitionnement, qu'il soit utilisé seul ou non.
Il est possible de reconditionner la FPU pour faire des multiplications entières. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits. Il est possible d'utiliser de multiplieur pour faire des multiplications entières 32 bits. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
Le reconditionnement de la FPU permet d'exécuter une multiplication entière en plus des autres opérations entières. Par exemple, prenons un CPU avec double émission entière-flottante, sans multiplieur entier séparé. Le reconditionnement de la FPU permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine.
Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants.
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''. Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, 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]]
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications.
Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires modernes utilisent toutes ces stratégies===
Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autre unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents.
* Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU.
* La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants.
* La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables.
De nos jours, le partitionnement est utilisé en complément de la duplication des ALU entières, et de la duplication des autres unités. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993.
Le meilleur moyen de voir comment sont combinés ces quatre technique est de regarder ce qu'il s'est passé historiquement.
==L'évolution historique de la superscalarité==
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. La plupart des CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 sont rapidement montés à de la quadruple émission, avec quelques exceptions. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 ||
|-
! 1995
| 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000
|-
! 1996
| class="f_bleu | 620 || || || || class="f_bleu | 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
===Les processeurs historiques à double émission===
Pour commencer, étudions le cas des premiers CPU superscalaires à double émission. Ils sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Notamment, la duplication des autres unités ne sert pour ainsi dire à rien, ce qui limite les possibilités, mais rendra les explications plus simples.
Il pouvaient se classer en deux/trois types principaux, assez stricts, avec quelques processeurs qui ne rentraient pas dans ces cases. Le premier utilise le partitionnement seul, le second dupliquer les ALU entières sans partitionnement, et le troisième type est volontairement passé sous silence pour le moment (nous aborderons le troisième type dans la partie sur des design à quadruple émission).
Les CPU n'utilisant que le partitionnement faisaient la double émission avec trois unités : une ALU, une FPU et une unité mémoire. Si la moitié des processeurs faisait du ''partitionnement complet'' entre ALU, FPU et unité mémoire, l'autre moitié utilisait la bouble émission entière-flottante.
Pour résumer, voici le tableau chronologique disant quel CPU utilisait quelle technique.
* Le partitionnement INT-FLOAT-MEM est en jaune.
* La double émission entière-flottante est en vert.
* La double émission entière est en rouge.
* Les CPU barrés utilisent la quadruple émission et seront vu dans la section suivante.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| class="f_vert | RS/6000 || || || class="f_rouge | i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| class="f_vert | RSC || class="f_vert | PA 7100 || SuperSPARC || || class="f_jaune | 21064 ||
|-
! 1993
| class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || ||
|-
! 1994
| <s>POWER 2</s> || PA 7200 || || || <s>21164</s> ||
|}
Vous voyez que certains processeur sont laissés de côté dans le tableau précédent. C'est car ils ne rentrent pas dans les catégories précédentes et faisaient autrement. Si on omet le Motorola 88110, qui utilisait une forme de double émission assez avancée, les deux CPUs qui vont nous intéresser sont le PA-RISC PA 7200 d'HP, et le SuperSPARC.
Le PA 7200, sorti en 1994, est un PA-7100 auquel on aurait rajouté une ALU entière. Précisons que ce processeur utilisait aussi le reconditionnement de la FPU, comme son prédécesseur. Les combinaisons autorisées étaient donc INT + INT, INT + MEM, INT + FLOAT/MUL, MEM + FLOAT/MUL. Les combinaisons autorisées par le partitionnement INT-FLOAT-MEM étaient donc possibles, en plus des combinaisons INT + INT, voire INT + INT + MUL (vu que la FPU est reconditionnée). Autrement dit, il pouvait faire toute ce que permettait un CPU à double émission entière, plus les combinaisons FLOAT/MUL + MEM.
Le SuperSPARC était un CPU triple émission, avec deux ALU entières, une FPU et une unité mémoire. Il pouvait émettre, à chaque cycle, trois µops en piochant dans deux opérations entières, un accès mémoire, une opération flottante. Le SuperSPARC avait de nombreuses contraintes d’appariement. Par exemple, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Une bonne partie de ces contraintes était lié au banc de registre, qui n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions.
: La seconde ALU a été abandonnée dans l'HyperSPARC, qui est de plus repassé en double émission, ce qui est une évolution à rebours de l'évolution des autres CPUs. Le SuperSPARC était en avance sur son temps, ce quelques années, alors que l'HyperSPARC a un design de son temps.
===Les processeurs historiques à quadruple émission===
C'est la même année , en 1994, que la quadruple émission est arrivée et que les CPU ont commencé à avoir de plus en plus d'unités de calcul. Même les designs restés en double émission avaient ajouté des unités de calcul. Il y avait alors trois stratégies pour la quadruple émission.
La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Les FPUs étaient soit dupliquées, soit scindées en deux en séparant l'additionneur du multiplieur flottant. Le POWER 2 dupliquait ses FPUs à la perfection, alors que l'21264 partitionnait la FPU. Les accès mémoire étaient traités comme des opérations entières, et le processeur pouvait : soit en faire un seul par cycle, soit en émettre deux, sous conditions (le cache devait être double port pour lire/écrire deux données à la fois, comme on le verra dans la suite du chapitre).
Les processeurs MIPS R8000 et R10000 allaient un peu plus loin, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités.
La seconde implémentation dupliquait l'ALU entière et combinait le tout avec un partitionnement INT-FLOAT-MEM. De quoi exécuter en même temps deux opérations entières, une opération flottante, et un accès mémoire. 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).
Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, avec au minimum deux ALU entières. Pour comprendre ce que je veux dire, prenons l'exemple de l'UltraSPARC. Il avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplier flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes. Le processeur pouvait émettre 4 instructions, tant qu'elles atterrissaient dans des unités différentes.
La même méthode peut être utilisée pour la double émission. Le Motorola 88110, sorti en 1991, était un CPU à double émission avec pas moins de 10 unités : deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise.
La morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes. Le tout est résumé dans le tableau ci-dessous, avec :
* en vert les CPU utilisant la première stratégie ;
* en rouge les CPU utilisant la seconde stratégie ;
* en jaune les CPU utilisant la troisième stratégie.
{|class="wikitable"
|+ Processeurs quadruple émission historiques
|-
! Année !! Processeur !! Unités
|-
! 1991
| class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques
|-
|-
! rowspan="2" | 1994
| class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'')
|-
| class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM
|-
! rowspan="3" | 1995
| class="f_jaune | PA 8000 || class="f_jaune | 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD
|-
| class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques
|-
| MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM
|-
! rowspan="2" | 1996
| class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM
|-
| class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL)
|}
===Les processeurs superscalaires modernes===
Les CPU superscalaires modernes ont des design légèrement différents des CPU historiques. Ils dupliquent l'unité entière, mais leur partitionnement est différent. En général, les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Par exemple, un CPU à 6 voies pourra émettre/exécuter 6 opérations entières simultanément, grâce à 6 ALU entières. Mais il s'agit là d'un maximum et certaines peuvent être remplacées par une opération flottante ou mémoire, qui utilisent l'unité mémoire et/ou la FPU.
Un exemple assez parlant est les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon. C'était une microarchitecture à triple émission, avec trois ALU entières et trois unités mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements.
Il s'agit là d'une tendance, cependant. Les CPU superscalaires tendent à mettre le paquet niveau opérations arithmétiques, mais pas forcément au point de remplir la totalité de la largeur du processeur. La règle est que pour N voies, le processeur dispose d'au moins N/2 ALU entières. Attention, cela ne signifie pas que le processeur émet systématiquement N/2 opération entières. Il peut par exemple émettre une combinaison d'opérations flottantes et mémoire, avec des branchements. Et N/2 est une borne basse, la plupart des CPU x86 ont environ tournent plutôt autour des 2/3, voire des 3/4.
En général, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. La conséquence est que les unités mémoire sont dupliquées, en seconde place dans l'ordre d'importance. Les processeurs x86 ont commencé à dupliquer leurs unités mémoire dès qu'ils ont atteint la quadruple émission, pour les CPU grand public. La microarchitecture Core d'Intel utilisait la quadruple émission, avec deux unités mémoire et trois ALU entières. Les microarchitectures d'AMD K5 et K6 utilisaient aussi la quadruple émission et avaient deux ALUs pour deux unités mémoire.
Plus récemment, la microarchitecture ''Golden Cove'' dispose de cinq unités de calcul trois unités mémoire (deux spécialisées dans les écritures, trois dans les lectures). La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire.Il y a donc soit égalité, soit avantage pour les ALU entières.
Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair.
==L'unité d'émission d'un processeur superscalaire==
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Voyons pourquoi.
===L'excès de ports d'émission===
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ou 5 ?
Une solution est de réduire la largeur du processeur. Dans l'exemple précédent, il est possible de passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La solution précédente est certes très intéressante, mais elle a des défauts. Le principal est que les ports d'émission sont sous-utilisés. Et les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
===L'intérieur de l'unité d'émission===
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Le banc de registre d'un processeur superscalaire===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Une autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions auto-aligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions auto-aligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez longs, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
fhpusqe2iflwh6v42q7uxo6iy6hdh21
773154
773153
2026-09-25T19:08:18Z
Mewtow
31375
/* Les unités de calcul d'un processeur superscalaire */
773154
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===L'impact des contraintes d’appariement sur la performance===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour la suite du chapitre===
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==Le ''front-end'' des processeurs superscalaires==
Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple.
Intuitivement, charger, décoder et 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 certaines processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. 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
|}
Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs.
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
==Les processeurs implémentés avec plusieurs pipelines==
Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Vous vous dites que l'implémentation d'un processeur superscalaire n'est pas la même suivant que le CPU utilise l'exécution dans le désordre, ou non. Mais dans les faits, la distinction est plus subtile que ca. Pour l'expliquer, je vais utiliser une classification originale, qui sépare les processeurs superscalaires en trois sous-types. La distinction se fait sur la manière dont la superscalarité est implémentée en matériel.La classification que j'utilise distingue :
* Les processeurs avec plusieurs pipelines, sans exécution dans le désordre.
* Les processeurs superscalaires avec un pipeline dynamique.
* Les processeurs avec des fenêtres d'instruction décentralisées.
La distinction n'est pas qu'une question d'implémentation matérielle. Elle impacte aussi le ratio entre instructions chargées/décodées et instructions émises. Vous vous dites que les deux sont égales, mais ce n'est le cas que sur les processeurs avec N pipelines ! Sur les autres processeurs, il est possible de décoder 4 instructions, mais d'en émettre maximum 6 par cycle ! Et sur les CPU avec des fenêtres d'instruction décentralisées, il y a aussi une différence entre le nombre d'instructions qui sont ''dispatch'' et ''schedule''.
La catégorie la plus simple est celle des '''processeurs avec plusieurs pipelines'''. Ce sont des processeurs superscalaires sans exécution dans le désordre. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction.
Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, d'une FPU, éventuellement d'un second banc de registres, guère plus. Il permet d'exécuter une seconde opération de calcul, pas plus, ce qui est à l'origine de contraintes d'appariemment assez fortes. De plus, il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section.
===La double émission entière-flottante===
L'exemple le plus simple utilise un pipeline pour les opérations flottantes, séparé du pipeline originel. Pour bien expliquer les choses, prenons le processeur illustré ci-dessous. Un processeur a deux ''back-end'' séparés, un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par l'ALU entière, passent éventuellement dans l'unité mémoire, pour ensuite arriver à l'étape de ''writeback''. La FPU et le banc de registre flottant ont eux aussi leur propre ''back-end'' séparé.
[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]]
Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaqaue pipeline, à chaque cycle.
L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
===La double émission entière===
Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait.
Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
==Les unités de calcul d'un processeur superscalaire==
Après avoir vu le chargement et le décodage superscalaire, voyons ce qu'il en est au niveau des unités de calcul. Nous allons parler des unités de calcul avant de parler de l'émission, pour une raison simple : l'émission fait l'interface entre unité de calcul et ''front-end'' et on ne peut pas parler de l'émission sans avoir vu comment la superscalarité impacte les deux. Si le ''front-end'' charge et décode un paquet de N instructions, son exécution demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est possible d'exploiter plusieurs unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu.
Les tout premiers CPU superscalaires étaient nombreux à utiliser cette technique. Ils étaient des CPU double émission, avec une ALU entière, une unité mémoire et une FPU. Nous parlerons de '''partitionnement INT-FLOAT-MEM'''. Il s'agit de l'HyperSPARC, de l'alpha 21064 et du PowerPC 603. Les contrainte d’appariement variaient grandement suivant le processeur. Les plus permissifs autorisaient toutes les combinaisons possibles entre : une opération entière, une opération flottante, et une unité mémoire. Mais ils n'étaient pas les seuls.
Le PowerPC 603 implémentait ce genre de double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions).
Il est aussi possible de faire la même chose pour les opérations entières, vu qu'elles sont prises en charge par trois circuits. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres. Il faut parfois lui ajouter le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible d'émettre une opération séparément, ce qui donne le '''partitionnement entier''' dans ce qui suit. Le partitionnement entier n'est jamais utilisé tel quel, car le gain en performance serait trop faible au regard des couts. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Le '''reconditionnement d'une unité fonctionnelle''' permet à celle-ci de faire des opérations qu'elle n'est pas prévue pour. Par exemple, il est possible d'utiliser l'unité mémoire comme seconde ALU entière, ou d'utiliser la FPU comme multiplieur. Ces deux exemples ne sont pas choisit au hasard, nous les étudierons dans ce qui suit. Le reconditionnement évite d'avoir à dupliquer une unité de calcul, tout en ajoutant une voie. Il permet d'améliorer l'efficacité du partitionnement, qu'il soit utilisé seul ou non.
Il est possible de reconditionner la FPU pour faire des multiplications entières. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits. Il est possible d'utiliser de multiplieur pour faire des multiplications entières 32 bits. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
Le reconditionnement de la FPU permet d'exécuter une multiplication entière en plus des autres opérations entières. Par exemple, prenons un CPU avec double émission entière-flottante, sans multiplieur entier séparé. Le reconditionnement de la FPU permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine.
Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants.
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''. Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, 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]]
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications.
Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires modernes utilisent toutes ces stratégies===
Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autre unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents.
* Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU.
* La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants.
* La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables.
De nos jours, le partitionnement est utilisé en complément de la duplication des ALU entières, et de la duplication des autres unités. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993.
Le meilleur moyen de voir comment sont combinés ces quatre technique est de regarder ce qu'il s'est passé historiquement.
==L'évolution historique de la superscalarité==
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. La plupart des CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 sont rapidement montés à de la quadruple émission, avec quelques exceptions. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 ||
|-
! 1995
| 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000
|-
! 1996
| class="f_bleu | 620 || || || || class="f_bleu | 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
===Les processeurs historiques à double émission===
Pour commencer, étudions le cas des premiers CPU superscalaires à double émission. Ils sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Notamment, la duplication des autres unités ne sert pour ainsi dire à rien, ce qui limite les possibilités, mais rendra les explications plus simples.
Il pouvaient se classer en deux/trois types principaux, assez stricts, avec quelques processeurs qui ne rentraient pas dans ces cases. Le premier utilise le partitionnement seul, le second dupliquer les ALU entières sans partitionnement, et le troisième type est volontairement passé sous silence pour le moment (nous aborderons le troisième type dans la partie sur des design à quadruple émission).
Les CPU n'utilisant que le partitionnement faisaient la double émission avec trois unités : une ALU, une FPU et une unité mémoire. Si la moitié des processeurs faisait du ''partitionnement complet'' entre ALU, FPU et unité mémoire, l'autre moitié utilisait la bouble émission entière-flottante.
Pour résumer, voici le tableau chronologique disant quel CPU utilisait quelle technique.
* Le partitionnement INT-FLOAT-MEM est en jaune.
* La double émission entière-flottante est en vert.
* La double émission entière est en rouge.
* Les CPU barrés utilisent la quadruple émission et seront vu dans la section suivante.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| class="f_vert | RS/6000 || || || class="f_rouge | i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| class="f_vert | RSC || class="f_vert | PA 7100 || SuperSPARC || || class="f_jaune | 21064 ||
|-
! 1993
| class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || ||
|-
! 1994
| <s>POWER 2</s> || PA 7200 || || || <s>21164</s> ||
|}
Vous voyez que certains processeur sont laissés de côté dans le tableau précédent. C'est car ils ne rentrent pas dans les catégories précédentes et faisaient autrement. Si on omet le Motorola 88110, qui utilisait une forme de double émission assez avancée, les deux CPUs qui vont nous intéresser sont le PA-RISC PA 7200 d'HP, et le SuperSPARC.
Le PA 7200, sorti en 1994, est un PA-7100 auquel on aurait rajouté une ALU entière. Précisons que ce processeur utilisait aussi le reconditionnement de la FPU, comme son prédécesseur. Les combinaisons autorisées étaient donc INT + INT, INT + MEM, INT + FLOAT/MUL, MEM + FLOAT/MUL. Les combinaisons autorisées par le partitionnement INT-FLOAT-MEM étaient donc possibles, en plus des combinaisons INT + INT, voire INT + INT + MUL (vu que la FPU est reconditionnée). Autrement dit, il pouvait faire toute ce que permettait un CPU à double émission entière, plus les combinaisons FLOAT/MUL + MEM.
Le SuperSPARC était un CPU triple émission, avec deux ALU entières, une FPU et une unité mémoire. Il pouvait émettre, à chaque cycle, trois µops en piochant dans deux opérations entières, un accès mémoire, une opération flottante. Le SuperSPARC avait de nombreuses contraintes d’appariement. Par exemple, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Une bonne partie de ces contraintes était lié au banc de registre, qui n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions.
: La seconde ALU a été abandonnée dans l'HyperSPARC, qui est de plus repassé en double émission, ce qui est une évolution à rebours de l'évolution des autres CPUs. Le SuperSPARC était en avance sur son temps, ce quelques années, alors que l'HyperSPARC a un design de son temps.
===Les processeurs historiques à quadruple émission===
C'est la même année , en 1994, que la quadruple émission est arrivée et que les CPU ont commencé à avoir de plus en plus d'unités de calcul. Même les designs restés en double émission avaient ajouté des unités de calcul. Il y avait alors trois stratégies pour la quadruple émission.
La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Les FPUs étaient soit dupliquées, soit scindées en deux en séparant l'additionneur du multiplieur flottant. Le POWER 2 dupliquait ses FPUs à la perfection, alors que l'21264 partitionnait la FPU. Les accès mémoire étaient traités comme des opérations entières, et le processeur pouvait : soit en faire un seul par cycle, soit en émettre deux, sous conditions (le cache devait être double port pour lire/écrire deux données à la fois, comme on le verra dans la suite du chapitre).
Les processeurs MIPS R8000 et R10000 allaient un peu plus loin, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités.
La seconde implémentation dupliquait l'ALU entière et combinait le tout avec un partitionnement INT-FLOAT-MEM. De quoi exécuter en même temps deux opérations entières, une opération flottante, et un accès mémoire. 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).
Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, avec au minimum deux ALU entières. Pour comprendre ce que je veux dire, prenons l'exemple de l'UltraSPARC. Il avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplier flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes. Le processeur pouvait émettre 4 instructions, tant qu'elles atterrissaient dans des unités différentes.
La même méthode peut être utilisée pour la double émission. Le Motorola 88110, sorti en 1991, était un CPU à double émission avec pas moins de 10 unités : deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise.
La morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes. Le tout est résumé dans le tableau ci-dessous, avec :
* en vert les CPU utilisant la première stratégie ;
* en rouge les CPU utilisant la seconde stratégie ;
* en jaune les CPU utilisant la troisième stratégie.
{|class="wikitable"
|+ Processeurs quadruple émission historiques
|-
! Année !! Processeur !! Unités
|-
! 1991
| class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques
|-
|-
! rowspan="2" | 1994
| class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'')
|-
| class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM
|-
! rowspan="3" | 1995
| class="f_jaune | PA 8000 || class="f_jaune | 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD
|-
| class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques
|-
| MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM
|-
! rowspan="2" | 1996
| class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM
|-
| class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL)
|}
===Les processeurs superscalaires modernes===
Les CPU superscalaires modernes ont des design légèrement différents des CPU historiques. Ils dupliquent l'unité entière, mais leur partitionnement est différent. En général, les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Par exemple, un CPU à 6 voies pourra émettre/exécuter 6 opérations entières simultanément, grâce à 6 ALU entières. Mais il s'agit là d'un maximum et certaines peuvent être remplacées par une opération flottante ou mémoire, qui utilisent l'unité mémoire et/ou la FPU.
Un exemple assez parlant est les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon. C'était une microarchitecture à triple émission, avec trois ALU entières et trois unités mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements.
Il s'agit là d'une tendance, cependant. Les CPU superscalaires tendent à mettre le paquet niveau opérations arithmétiques, mais pas forcément au point de remplir la totalité de la largeur du processeur. La règle est que pour N voies, le processeur dispose d'au moins N/2 ALU entières. Attention, cela ne signifie pas que le processeur émet systématiquement N/2 opération entières. Il peut par exemple émettre une combinaison d'opérations flottantes et mémoire, avec des branchements. Et N/2 est une borne basse, la plupart des CPU x86 ont environ tournent plutôt autour des 2/3, voire des 3/4.
En général, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. La conséquence est que les unités mémoire sont dupliquées, en seconde place dans l'ordre d'importance. Les processeurs x86 ont commencé à dupliquer leurs unités mémoire dès qu'ils ont atteint la quadruple émission, pour les CPU grand public. La microarchitecture Core d'Intel utilisait la quadruple émission, avec deux unités mémoire et trois ALU entières. Les microarchitectures d'AMD K5 et K6 utilisaient aussi la quadruple émission et avaient deux ALUs pour deux unités mémoire.
Plus récemment, la microarchitecture ''Golden Cove'' dispose de cinq unités de calcul trois unités mémoire (deux spécialisées dans les écritures, trois dans les lectures). La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire.Il y a donc soit égalité, soit avantage pour les ALU entières.
Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair.
==L'unité d'émission d'un processeur superscalaire==
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Voyons pourquoi.
===L'excès de ports d'émission===
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ou 5 ?
Une solution est de réduire la largeur du processeur. Dans l'exemple précédent, il est possible de passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La solution précédente est certes très intéressante, mais elle a des défauts. Le principal est que les ports d'émission sont sous-utilisés. Et les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
===L'intérieur de l'unité d'émission===
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Le banc de registre d'un processeur superscalaire===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Une autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions auto-aligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions auto-aligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez longs, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
1rk83ac9jeunv6gftm2dnlgl1kjttpf
773155
773154
2026-09-25T19:09:19Z
Mewtow
31375
/* Les processeurs historiques à double émission */
773155
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===L'impact des contraintes d’appariement sur la performance===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour la suite du chapitre===
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==Le ''front-end'' des processeurs superscalaires==
Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple.
Intuitivement, charger, décoder et 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 certaines processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. 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
|}
Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs.
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
==Les processeurs implémentés avec plusieurs pipelines==
Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Vous vous dites que l'implémentation d'un processeur superscalaire n'est pas la même suivant que le CPU utilise l'exécution dans le désordre, ou non. Mais dans les faits, la distinction est plus subtile que ca. Pour l'expliquer, je vais utiliser une classification originale, qui sépare les processeurs superscalaires en trois sous-types. La distinction se fait sur la manière dont la superscalarité est implémentée en matériel.La classification que j'utilise distingue :
* Les processeurs avec plusieurs pipelines, sans exécution dans le désordre.
* Les processeurs superscalaires avec un pipeline dynamique.
* Les processeurs avec des fenêtres d'instruction décentralisées.
La distinction n'est pas qu'une question d'implémentation matérielle. Elle impacte aussi le ratio entre instructions chargées/décodées et instructions émises. Vous vous dites que les deux sont égales, mais ce n'est le cas que sur les processeurs avec N pipelines ! Sur les autres processeurs, il est possible de décoder 4 instructions, mais d'en émettre maximum 6 par cycle ! Et sur les CPU avec des fenêtres d'instruction décentralisées, il y a aussi une différence entre le nombre d'instructions qui sont ''dispatch'' et ''schedule''.
La catégorie la plus simple est celle des '''processeurs avec plusieurs pipelines'''. Ce sont des processeurs superscalaires sans exécution dans le désordre. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction.
Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, d'une FPU, éventuellement d'un second banc de registres, guère plus. Il permet d'exécuter une seconde opération de calcul, pas plus, ce qui est à l'origine de contraintes d'appariemment assez fortes. De plus, il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section.
===La double émission entière-flottante===
L'exemple le plus simple utilise un pipeline pour les opérations flottantes, séparé du pipeline originel. Pour bien expliquer les choses, prenons le processeur illustré ci-dessous. Un processeur a deux ''back-end'' séparés, un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par l'ALU entière, passent éventuellement dans l'unité mémoire, pour ensuite arriver à l'étape de ''writeback''. La FPU et le banc de registre flottant ont eux aussi leur propre ''back-end'' séparé.
[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]]
Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaqaue pipeline, à chaque cycle.
L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
===La double émission entière===
Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait.
Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
==Les unités de calcul d'un processeur superscalaire==
Après avoir vu le chargement et le décodage superscalaire, voyons ce qu'il en est au niveau des unités de calcul. Nous allons parler des unités de calcul avant de parler de l'émission, pour une raison simple : l'émission fait l'interface entre unité de calcul et ''front-end'' et on ne peut pas parler de l'émission sans avoir vu comment la superscalarité impacte les deux. Si le ''front-end'' charge et décode un paquet de N instructions, son exécution demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est possible d'exploiter plusieurs unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu.
Les tout premiers CPU superscalaires étaient nombreux à utiliser cette technique. Ils étaient des CPU double émission, avec une ALU entière, une unité mémoire et une FPU. Nous parlerons de '''partitionnement INT-FLOAT-MEM'''. Il s'agit de l'HyperSPARC, de l'alpha 21064 et du PowerPC 603. Les contrainte d’appariement variaient grandement suivant le processeur. Les plus permissifs autorisaient toutes les combinaisons possibles entre : une opération entière, une opération flottante, et une unité mémoire. Mais ils n'étaient pas les seuls.
Le PowerPC 603 implémentait ce genre de double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions).
Il est aussi possible de faire la même chose pour les opérations entières, vu qu'elles sont prises en charge par trois circuits. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres. Il faut parfois lui ajouter le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible d'émettre une opération séparément, ce qui donne le '''partitionnement entier''' dans ce qui suit. Le partitionnement entier n'est jamais utilisé tel quel, car le gain en performance serait trop faible au regard des couts. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Le '''reconditionnement d'une unité fonctionnelle''' permet à celle-ci de faire des opérations qu'elle n'est pas prévue pour. Par exemple, il est possible d'utiliser l'unité mémoire comme seconde ALU entière, ou d'utiliser la FPU comme multiplieur. Ces deux exemples ne sont pas choisit au hasard, nous les étudierons dans ce qui suit. Le reconditionnement évite d'avoir à dupliquer une unité de calcul, tout en ajoutant une voie. Il permet d'améliorer l'efficacité du partitionnement, qu'il soit utilisé seul ou non.
Il est possible de reconditionner la FPU pour faire des multiplications entières. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits. Il est possible d'utiliser de multiplieur pour faire des multiplications entières 32 bits. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
Le reconditionnement de la FPU permet d'exécuter une multiplication entière en plus des autres opérations entières. Par exemple, prenons un CPU avec double émission entière-flottante, sans multiplieur entier séparé. Le reconditionnement de la FPU permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine.
Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants.
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''. Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, 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]]
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications.
Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires modernes utilisent toutes ces stratégies===
Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autre unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents.
* Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU.
* La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants.
* La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables.
De nos jours, le partitionnement est utilisé en complément de la duplication des ALU entières, et de la duplication des autres unités. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993.
Le meilleur moyen de voir comment sont combinés ces quatre technique est de regarder ce qu'il s'est passé historiquement.
==L'évolution historique de la superscalarité==
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. La plupart des CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 sont rapidement montés à de la quadruple émission, avec quelques exceptions. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 ||
|-
! 1995
| 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000
|-
! 1996
| class="f_bleu | 620 || || || || class="f_bleu | 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
===Les processeurs historiques à double émission===
Pour commencer, étudions le cas des premiers CPU superscalaires à double émission. Ils sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Notamment, la duplication des autres unités ne sert pour ainsi dire à rien, ce qui limite les possibilités, mais rendra les explications plus simples.
Il pouvaient se classer en deux/trois types principaux, assez stricts, avec quelques processeurs qui ne rentraient pas dans ces cases. Le premier utilise le partitionnement seul, le second dupliquer les ALU entières sans partitionnement, et le troisième type est volontairement passé sous silence pour le moment (nous aborderons le troisième type dans la partie sur des design à quadruple émission).
Les CPU n'utilisant que le partitionnement faisaient la double émission avec trois unités : une ALU, une FPU et une unité mémoire. Si la moitié des processeurs faisait du ''partitionnement complet'' entre ALU, FPU et unité mémoire, l'autre moitié utilisait la bouble émission entière-flottante.
Pour résumer, voici le tableau chronologique disant quel CPU utilisait quelle technique.
* Le partitionnement INT-FLOAT-MEM est en jaune.
* La double émission entière-flottante est en vert.
* La double émission entière est en rouge.
* Les CPU barrés utilisent la quadruple émission et seront vu dans la section suivante.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| class="f_vert | RS/6000 || || || class="f_rouge | i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| class="f_vert | RSC || class="f_vert | PA 7100 || SuperSPARC || || class="f_jaune | 21064 ||
|-
! 1993
| class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || ||
|-
! 1994
| <s>POWER 2</s> || PA 7200 || || || <s>21164</s> ||
|}
Vous voyez que certains processeur sont laissés de côté dans le tableau précédent. C'est car ils ne rentrent pas dans les catégories précédentes et faisaient autrement. Si on omet le Motorola 88110, qui utilisait une forme de double émission assez avancée, les deux CPUs qui vont nous intéresser sont le PA-RISC PA 7200 d'HP, et le SuperSPARC.
Le PA 7200, sorti en 1994, est un PA-7100 auquel on aurait rajouté une ALU entière. Précisons que ce processeur exécutait les multiplications entières dans la FPU, comme son prédécesseur. Les combinaisons autorisées étaient donc INT + INT, INT + MEM, INT + FLOAT/MUL, MEM + FLOAT/MUL. Les combinaisons autorisées par le partitionnement INT-FLOAT-MEM étaient donc possibles, en plus des combinaisons INT + INT et INT + INT + MUL. Autrement dit, il pouvait faire toute ce que permettait un CPU à double émission entière, plus les combinaisons FLOAT/MUL + MEM.
Le SuperSPARC était un CPU triple émission, avec deux ALU entières, une FPU et une unité mémoire. Il pouvait émettre, à chaque cycle, trois µops en piochant dans deux opérations entières, un accès mémoire, une opération flottante. Le SuperSPARC avait de nombreuses contraintes d’appariement. Par exemple, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Une bonne partie de ces contraintes était lié au banc de registre, qui n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions.
: La seconde ALU a été abandonnée dans l'HyperSPARC, qui est de plus repassé en double émission, ce qui est une évolution à rebours de l'évolution des autres CPUs. Le SuperSPARC était en avance sur son temps, ce quelques années, alors que l'HyperSPARC a un design de son temps.
===Les processeurs historiques à quadruple émission===
C'est la même année , en 1994, que la quadruple émission est arrivée et que les CPU ont commencé à avoir de plus en plus d'unités de calcul. Même les designs restés en double émission avaient ajouté des unités de calcul. Il y avait alors trois stratégies pour la quadruple émission.
La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Les FPUs étaient soit dupliquées, soit scindées en deux en séparant l'additionneur du multiplieur flottant. Le POWER 2 dupliquait ses FPUs à la perfection, alors que l'21264 partitionnait la FPU. Les accès mémoire étaient traités comme des opérations entières, et le processeur pouvait : soit en faire un seul par cycle, soit en émettre deux, sous conditions (le cache devait être double port pour lire/écrire deux données à la fois, comme on le verra dans la suite du chapitre).
Les processeurs MIPS R8000 et R10000 allaient un peu plus loin, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités.
La seconde implémentation dupliquait l'ALU entière et combinait le tout avec un partitionnement INT-FLOAT-MEM. De quoi exécuter en même temps deux opérations entières, une opération flottante, et un accès mémoire. 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).
Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, avec au minimum deux ALU entières. Pour comprendre ce que je veux dire, prenons l'exemple de l'UltraSPARC. Il avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplier flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes. Le processeur pouvait émettre 4 instructions, tant qu'elles atterrissaient dans des unités différentes.
La même méthode peut être utilisée pour la double émission. Le Motorola 88110, sorti en 1991, était un CPU à double émission avec pas moins de 10 unités : deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise.
La morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes. Le tout est résumé dans le tableau ci-dessous, avec :
* en vert les CPU utilisant la première stratégie ;
* en rouge les CPU utilisant la seconde stratégie ;
* en jaune les CPU utilisant la troisième stratégie.
{|class="wikitable"
|+ Processeurs quadruple émission historiques
|-
! Année !! Processeur !! Unités
|-
! 1991
| class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques
|-
|-
! rowspan="2" | 1994
| class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'')
|-
| class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM
|-
! rowspan="3" | 1995
| class="f_jaune | PA 8000 || class="f_jaune | 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD
|-
| class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques
|-
| MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM
|-
! rowspan="2" | 1996
| class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM
|-
| class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL)
|}
===Les processeurs superscalaires modernes===
Les CPU superscalaires modernes ont des design légèrement différents des CPU historiques. Ils dupliquent l'unité entière, mais leur partitionnement est différent. En général, les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Par exemple, un CPU à 6 voies pourra émettre/exécuter 6 opérations entières simultanément, grâce à 6 ALU entières. Mais il s'agit là d'un maximum et certaines peuvent être remplacées par une opération flottante ou mémoire, qui utilisent l'unité mémoire et/ou la FPU.
Un exemple assez parlant est les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon. C'était une microarchitecture à triple émission, avec trois ALU entières et trois unités mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements.
Il s'agit là d'une tendance, cependant. Les CPU superscalaires tendent à mettre le paquet niveau opérations arithmétiques, mais pas forcément au point de remplir la totalité de la largeur du processeur. La règle est que pour N voies, le processeur dispose d'au moins N/2 ALU entières. Attention, cela ne signifie pas que le processeur émet systématiquement N/2 opération entières. Il peut par exemple émettre une combinaison d'opérations flottantes et mémoire, avec des branchements. Et N/2 est une borne basse, la plupart des CPU x86 ont environ tournent plutôt autour des 2/3, voire des 3/4.
En général, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. La conséquence est que les unités mémoire sont dupliquées, en seconde place dans l'ordre d'importance. Les processeurs x86 ont commencé à dupliquer leurs unités mémoire dès qu'ils ont atteint la quadruple émission, pour les CPU grand public. La microarchitecture Core d'Intel utilisait la quadruple émission, avec deux unités mémoire et trois ALU entières. Les microarchitectures d'AMD K5 et K6 utilisaient aussi la quadruple émission et avaient deux ALUs pour deux unités mémoire.
Plus récemment, la microarchitecture ''Golden Cove'' dispose de cinq unités de calcul trois unités mémoire (deux spécialisées dans les écritures, trois dans les lectures). La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire.Il y a donc soit égalité, soit avantage pour les ALU entières.
Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair.
==L'unité d'émission d'un processeur superscalaire==
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Voyons pourquoi.
===L'excès de ports d'émission===
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ou 5 ?
Une solution est de réduire la largeur du processeur. Dans l'exemple précédent, il est possible de passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La solution précédente est certes très intéressante, mais elle a des défauts. Le principal est que les ports d'émission sont sous-utilisés. Et les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
===L'intérieur de l'unité d'émission===
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Le banc de registre d'un processeur superscalaire===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Une autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions auto-aligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions auto-aligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez longs, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
po1exdo4ozus414gn7asbwdj1sab97x
773156
773155
2026-09-25T19:10:25Z
Mewtow
31375
/* Le reconditionnement des unités fonctionnelles */
773156
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===L'impact des contraintes d’appariement sur la performance===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour la suite du chapitre===
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==Le ''front-end'' des processeurs superscalaires==
Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple.
Intuitivement, charger, décoder et 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 certaines processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. 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
|}
Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs.
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
==Les processeurs implémentés avec plusieurs pipelines==
Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Vous vous dites que l'implémentation d'un processeur superscalaire n'est pas la même suivant que le CPU utilise l'exécution dans le désordre, ou non. Mais dans les faits, la distinction est plus subtile que ca. Pour l'expliquer, je vais utiliser une classification originale, qui sépare les processeurs superscalaires en trois sous-types. La distinction se fait sur la manière dont la superscalarité est implémentée en matériel.La classification que j'utilise distingue :
* Les processeurs avec plusieurs pipelines, sans exécution dans le désordre.
* Les processeurs superscalaires avec un pipeline dynamique.
* Les processeurs avec des fenêtres d'instruction décentralisées.
La distinction n'est pas qu'une question d'implémentation matérielle. Elle impacte aussi le ratio entre instructions chargées/décodées et instructions émises. Vous vous dites que les deux sont égales, mais ce n'est le cas que sur les processeurs avec N pipelines ! Sur les autres processeurs, il est possible de décoder 4 instructions, mais d'en émettre maximum 6 par cycle ! Et sur les CPU avec des fenêtres d'instruction décentralisées, il y a aussi une différence entre le nombre d'instructions qui sont ''dispatch'' et ''schedule''.
La catégorie la plus simple est celle des '''processeurs avec plusieurs pipelines'''. Ce sont des processeurs superscalaires sans exécution dans le désordre. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction.
Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, d'une FPU, éventuellement d'un second banc de registres, guère plus. Il permet d'exécuter une seconde opération de calcul, pas plus, ce qui est à l'origine de contraintes d'appariemment assez fortes. De plus, il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section.
===La double émission entière-flottante===
L'exemple le plus simple utilise un pipeline pour les opérations flottantes, séparé du pipeline originel. Pour bien expliquer les choses, prenons le processeur illustré ci-dessous. Un processeur a deux ''back-end'' séparés, un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par l'ALU entière, passent éventuellement dans l'unité mémoire, pour ensuite arriver à l'étape de ''writeback''. La FPU et le banc de registre flottant ont eux aussi leur propre ''back-end'' séparé.
[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]]
Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaqaue pipeline, à chaque cycle.
L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
===La double émission entière===
Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait.
Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
==Les unités de calcul d'un processeur superscalaire==
Après avoir vu le chargement et le décodage superscalaire, voyons ce qu'il en est au niveau des unités de calcul. Nous allons parler des unités de calcul avant de parler de l'émission, pour une raison simple : l'émission fait l'interface entre unité de calcul et ''front-end'' et on ne peut pas parler de l'émission sans avoir vu comment la superscalarité impacte les deux. Si le ''front-end'' charge et décode un paquet de N instructions, son exécution demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est possible d'exploiter plusieurs unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu.
Les tout premiers CPU superscalaires étaient nombreux à utiliser cette technique. Ils étaient des CPU double émission, avec une ALU entière, une unité mémoire et une FPU. Nous parlerons de '''partitionnement INT-FLOAT-MEM'''. Il s'agit de l'HyperSPARC, de l'alpha 21064 et du PowerPC 603. Les contrainte d’appariement variaient grandement suivant le processeur. Les plus permissifs autorisaient toutes les combinaisons possibles entre : une opération entière, une opération flottante, et une unité mémoire. Mais ils n'étaient pas les seuls.
Le PowerPC 603 implémentait ce genre de double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions).
Il est aussi possible de faire la même chose pour les opérations entières, vu qu'elles sont prises en charge par trois circuits. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres. Il faut parfois lui ajouter le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible d'émettre une opération séparément, ce qui donne le '''partitionnement entier''' dans ce qui suit. Le partitionnement entier n'est jamais utilisé tel quel, car le gain en performance serait trop faible au regard des couts. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''. Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, 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]]
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications.
Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires modernes utilisent toutes ces stratégies===
Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autre unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents.
* Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU.
* La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants.
* La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables.
De nos jours, le partitionnement est utilisé en complément de la duplication des ALU entières, et de la duplication des autres unités. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993.
Le meilleur moyen de voir comment sont combinés ces quatre technique est de regarder ce qu'il s'est passé historiquement.
==L'évolution historique de la superscalarité==
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. La plupart des CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 sont rapidement montés à de la quadruple émission, avec quelques exceptions. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 ||
|-
! 1995
| 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000
|-
! 1996
| class="f_bleu | 620 || || || || class="f_bleu | 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
===Les processeurs historiques à double émission===
Pour commencer, étudions le cas des premiers CPU superscalaires à double émission. Ils sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Notamment, la duplication des autres unités ne sert pour ainsi dire à rien, ce qui limite les possibilités, mais rendra les explications plus simples.
Il pouvaient se classer en deux/trois types principaux, assez stricts, avec quelques processeurs qui ne rentraient pas dans ces cases. Le premier utilise le partitionnement seul, le second dupliquer les ALU entières sans partitionnement, et le troisième type est volontairement passé sous silence pour le moment (nous aborderons le troisième type dans la partie sur des design à quadruple émission).
Les CPU n'utilisant que le partitionnement faisaient la double émission avec trois unités : une ALU, une FPU et une unité mémoire. Si la moitié des processeurs faisait du ''partitionnement complet'' entre ALU, FPU et unité mémoire, l'autre moitié utilisait la bouble émission entière-flottante.
Pour résumer, voici le tableau chronologique disant quel CPU utilisait quelle technique.
* Le partitionnement INT-FLOAT-MEM est en jaune.
* La double émission entière-flottante est en vert.
* La double émission entière est en rouge.
* Les CPU barrés utilisent la quadruple émission et seront vu dans la section suivante.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| class="f_vert | RS/6000 || || || class="f_rouge | i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| class="f_vert | RSC || class="f_vert | PA 7100 || SuperSPARC || || class="f_jaune | 21064 ||
|-
! 1993
| class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || ||
|-
! 1994
| <s>POWER 2</s> || PA 7200 || || || <s>21164</s> ||
|}
Vous voyez que certains processeur sont laissés de côté dans le tableau précédent. C'est car ils ne rentrent pas dans les catégories précédentes et faisaient autrement. Si on omet le Motorola 88110, qui utilisait une forme de double émission assez avancée, les deux CPUs qui vont nous intéresser sont le PA-RISC PA 7200 d'HP, et le SuperSPARC.
Le PA 7200, sorti en 1994, est un PA-7100 auquel on aurait rajouté une ALU entière. Précisons que ce processeur exécutait les multiplications entières dans la FPU, comme son prédécesseur. Les combinaisons autorisées étaient donc INT + INT, INT + MEM, INT + FLOAT/MUL, MEM + FLOAT/MUL. Les combinaisons autorisées par le partitionnement INT-FLOAT-MEM étaient donc possibles, en plus des combinaisons INT + INT et INT + INT + MUL. Autrement dit, il pouvait faire toute ce que permettait un CPU à double émission entière, plus les combinaisons FLOAT/MUL + MEM.
Le SuperSPARC était un CPU triple émission, avec deux ALU entières, une FPU et une unité mémoire. Il pouvait émettre, à chaque cycle, trois µops en piochant dans deux opérations entières, un accès mémoire, une opération flottante. Le SuperSPARC avait de nombreuses contraintes d’appariement. Par exemple, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Une bonne partie de ces contraintes était lié au banc de registre, qui n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions.
: La seconde ALU a été abandonnée dans l'HyperSPARC, qui est de plus repassé en double émission, ce qui est une évolution à rebours de l'évolution des autres CPUs. Le SuperSPARC était en avance sur son temps, ce quelques années, alors que l'HyperSPARC a un design de son temps.
===Les processeurs historiques à quadruple émission===
C'est la même année , en 1994, que la quadruple émission est arrivée et que les CPU ont commencé à avoir de plus en plus d'unités de calcul. Même les designs restés en double émission avaient ajouté des unités de calcul. Il y avait alors trois stratégies pour la quadruple émission.
La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Les FPUs étaient soit dupliquées, soit scindées en deux en séparant l'additionneur du multiplieur flottant. Le POWER 2 dupliquait ses FPUs à la perfection, alors que l'21264 partitionnait la FPU. Les accès mémoire étaient traités comme des opérations entières, et le processeur pouvait : soit en faire un seul par cycle, soit en émettre deux, sous conditions (le cache devait être double port pour lire/écrire deux données à la fois, comme on le verra dans la suite du chapitre).
Les processeurs MIPS R8000 et R10000 allaient un peu plus loin, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités.
La seconde implémentation dupliquait l'ALU entière et combinait le tout avec un partitionnement INT-FLOAT-MEM. De quoi exécuter en même temps deux opérations entières, une opération flottante, et un accès mémoire. 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).
Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, avec au minimum deux ALU entières. Pour comprendre ce que je veux dire, prenons l'exemple de l'UltraSPARC. Il avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplier flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes. Le processeur pouvait émettre 4 instructions, tant qu'elles atterrissaient dans des unités différentes.
La même méthode peut être utilisée pour la double émission. Le Motorola 88110, sorti en 1991, était un CPU à double émission avec pas moins de 10 unités : deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise.
La morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes. Le tout est résumé dans le tableau ci-dessous, avec :
* en vert les CPU utilisant la première stratégie ;
* en rouge les CPU utilisant la seconde stratégie ;
* en jaune les CPU utilisant la troisième stratégie.
{|class="wikitable"
|+ Processeurs quadruple émission historiques
|-
! Année !! Processeur !! Unités
|-
! 1991
| class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques
|-
|-
! rowspan="2" | 1994
| class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'')
|-
| class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM
|-
! rowspan="3" | 1995
| class="f_jaune | PA 8000 || class="f_jaune | 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD
|-
| class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques
|-
| MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM
|-
! rowspan="2" | 1996
| class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM
|-
| class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL)
|}
===Les processeurs superscalaires modernes===
Les CPU superscalaires modernes ont des design légèrement différents des CPU historiques. Ils dupliquent l'unité entière, mais leur partitionnement est différent. En général, les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Par exemple, un CPU à 6 voies pourra émettre/exécuter 6 opérations entières simultanément, grâce à 6 ALU entières. Mais il s'agit là d'un maximum et certaines peuvent être remplacées par une opération flottante ou mémoire, qui utilisent l'unité mémoire et/ou la FPU.
Un exemple assez parlant est les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon. C'était une microarchitecture à triple émission, avec trois ALU entières et trois unités mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements.
Il s'agit là d'une tendance, cependant. Les CPU superscalaires tendent à mettre le paquet niveau opérations arithmétiques, mais pas forcément au point de remplir la totalité de la largeur du processeur. La règle est que pour N voies, le processeur dispose d'au moins N/2 ALU entières. Attention, cela ne signifie pas que le processeur émet systématiquement N/2 opération entières. Il peut par exemple émettre une combinaison d'opérations flottantes et mémoire, avec des branchements. Et N/2 est une borne basse, la plupart des CPU x86 ont environ tournent plutôt autour des 2/3, voire des 3/4.
En général, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. La conséquence est que les unités mémoire sont dupliquées, en seconde place dans l'ordre d'importance. Les processeurs x86 ont commencé à dupliquer leurs unités mémoire dès qu'ils ont atteint la quadruple émission, pour les CPU grand public. La microarchitecture Core d'Intel utilisait la quadruple émission, avec deux unités mémoire et trois ALU entières. Les microarchitectures d'AMD K5 et K6 utilisaient aussi la quadruple émission et avaient deux ALUs pour deux unités mémoire.
Plus récemment, la microarchitecture ''Golden Cove'' dispose de cinq unités de calcul trois unités mémoire (deux spécialisées dans les écritures, trois dans les lectures). La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire.Il y a donc soit égalité, soit avantage pour les ALU entières.
Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair.
==L'unité d'émission d'un processeur superscalaire==
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Voyons pourquoi.
===L'excès de ports d'émission===
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ou 5 ?
Une solution est de réduire la largeur du processeur. Dans l'exemple précédent, il est possible de passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La solution précédente est certes très intéressante, mais elle a des défauts. Le principal est que les ports d'émission sont sous-utilisés. Et les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
===L'intérieur de l'unité d'émission===
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Le banc de registre d'un processeur superscalaire===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Une autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions auto-aligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions auto-aligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez longs, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
notbhhavqlhglk6345j6e4xtjhcyibi
773157
773156
2026-09-25T19:11:36Z
Mewtow
31375
/* La double émission entière-flottante */
773157
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===L'impact des contraintes d’appariement sur la performance===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour la suite du chapitre===
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==Le ''front-end'' des processeurs superscalaires==
Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple.
Intuitivement, charger, décoder et 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 certaines processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. 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
|}
Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs.
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
==Les processeurs implémentés avec plusieurs pipelines==
Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Vous vous dites que l'implémentation d'un processeur superscalaire n'est pas la même suivant que le CPU utilise l'exécution dans le désordre, ou non. Mais dans les faits, la distinction est plus subtile que ca. Pour l'expliquer, je vais utiliser une classification originale, qui sépare les processeurs superscalaires en trois sous-types. La distinction se fait sur la manière dont la superscalarité est implémentée en matériel.La classification que j'utilise distingue :
* Les processeurs avec plusieurs pipelines, sans exécution dans le désordre.
* Les processeurs superscalaires avec un pipeline dynamique.
* Les processeurs avec des fenêtres d'instruction décentralisées.
La distinction n'est pas qu'une question d'implémentation matérielle. Elle impacte aussi le ratio entre instructions chargées/décodées et instructions émises. Vous vous dites que les deux sont égales, mais ce n'est le cas que sur les processeurs avec N pipelines ! Sur les autres processeurs, il est possible de décoder 4 instructions, mais d'en émettre maximum 6 par cycle ! Et sur les CPU avec des fenêtres d'instruction décentralisées, il y a aussi une différence entre le nombre d'instructions qui sont ''dispatch'' et ''schedule''.
La catégorie la plus simple est celle des '''processeurs avec plusieurs pipelines'''. Ce sont des processeurs superscalaires sans exécution dans le désordre. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction.
Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, d'une FPU, éventuellement d'un second banc de registres, guère plus. Il permet d'exécuter une seconde opération de calcul, pas plus, ce qui est à l'origine de contraintes d'appariemment assez fortes. De plus, il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section.
===La double émission entière-flottante===
L'exemple le plus simple utilise un pipeline pour les opérations flottantes, séparé du pipeline originel. Pour bien expliquer les choses, prenons le processeur illustré ci-dessous. Un processeur a deux ''back-end'' séparés, un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par l'ALU entière, passent éventuellement dans l'unité mémoire, pour ensuite arriver à l'étape de ''writeback''. La FPU et le banc de registre flottant ont eux aussi leur propre ''back-end'' séparé.
[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]]
Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaqaue pipeline, à chaque cycle.
L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine.
Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants.
===La double émission entière===
Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait.
Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
==Les unités de calcul d'un processeur superscalaire==
Après avoir vu le chargement et le décodage superscalaire, voyons ce qu'il en est au niveau des unités de calcul. Nous allons parler des unités de calcul avant de parler de l'émission, pour une raison simple : l'émission fait l'interface entre unité de calcul et ''front-end'' et on ne peut pas parler de l'émission sans avoir vu comment la superscalarité impacte les deux. Si le ''front-end'' charge et décode un paquet de N instructions, son exécution demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est possible d'exploiter plusieurs unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu.
Les tout premiers CPU superscalaires étaient nombreux à utiliser cette technique. Ils étaient des CPU double émission, avec une ALU entière, une unité mémoire et une FPU. Nous parlerons de '''partitionnement INT-FLOAT-MEM'''. Il s'agit de l'HyperSPARC, de l'alpha 21064 et du PowerPC 603. Les contrainte d’appariement variaient grandement suivant le processeur. Les plus permissifs autorisaient toutes les combinaisons possibles entre : une opération entière, une opération flottante, et une unité mémoire. Mais ils n'étaient pas les seuls.
Le PowerPC 603 implémentait ce genre de double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions).
Il est aussi possible de faire la même chose pour les opérations entières, vu qu'elles sont prises en charge par trois circuits. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres. Il faut parfois lui ajouter le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible d'émettre une opération séparément, ce qui donne le '''partitionnement entier''' dans ce qui suit. Le partitionnement entier n'est jamais utilisé tel quel, car le gain en performance serait trop faible au regard des couts. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''. Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, 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]]
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications.
Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires modernes utilisent toutes ces stratégies===
Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autre unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents.
* Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU.
* La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants.
* La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables.
De nos jours, le partitionnement est utilisé en complément de la duplication des ALU entières, et de la duplication des autres unités. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993.
Le meilleur moyen de voir comment sont combinés ces quatre technique est de regarder ce qu'il s'est passé historiquement.
==L'évolution historique de la superscalarité==
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. La plupart des CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 sont rapidement montés à de la quadruple émission, avec quelques exceptions. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 ||
|-
! 1995
| 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000
|-
! 1996
| class="f_bleu | 620 || || || || class="f_bleu | 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
===Les processeurs historiques à double émission===
Pour commencer, étudions le cas des premiers CPU superscalaires à double émission. Ils sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Notamment, la duplication des autres unités ne sert pour ainsi dire à rien, ce qui limite les possibilités, mais rendra les explications plus simples.
Il pouvaient se classer en deux/trois types principaux, assez stricts, avec quelques processeurs qui ne rentraient pas dans ces cases. Le premier utilise le partitionnement seul, le second dupliquer les ALU entières sans partitionnement, et le troisième type est volontairement passé sous silence pour le moment (nous aborderons le troisième type dans la partie sur des design à quadruple émission).
Les CPU n'utilisant que le partitionnement faisaient la double émission avec trois unités : une ALU, une FPU et une unité mémoire. Si la moitié des processeurs faisait du ''partitionnement complet'' entre ALU, FPU et unité mémoire, l'autre moitié utilisait la bouble émission entière-flottante.
Pour résumer, voici le tableau chronologique disant quel CPU utilisait quelle technique.
* Le partitionnement INT-FLOAT-MEM est en jaune.
* La double émission entière-flottante est en vert.
* La double émission entière est en rouge.
* Les CPU barrés utilisent la quadruple émission et seront vu dans la section suivante.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| class="f_vert | RS/6000 || || || class="f_rouge | i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| class="f_vert | RSC || class="f_vert | PA 7100 || SuperSPARC || || class="f_jaune | 21064 ||
|-
! 1993
| class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || ||
|-
! 1994
| <s>POWER 2</s> || PA 7200 || || || <s>21164</s> ||
|}
Vous voyez que certains processeur sont laissés de côté dans le tableau précédent. C'est car ils ne rentrent pas dans les catégories précédentes et faisaient autrement. Si on omet le Motorola 88110, qui utilisait une forme de double émission assez avancée, les deux CPUs qui vont nous intéresser sont le PA-RISC PA 7200 d'HP, et le SuperSPARC.
Le PA 7200, sorti en 1994, est un PA-7100 auquel on aurait rajouté une ALU entière. Précisons que ce processeur exécutait les multiplications entières dans la FPU, comme son prédécesseur. Les combinaisons autorisées étaient donc INT + INT, INT + MEM, INT + FLOAT/MUL, MEM + FLOAT/MUL. Les combinaisons autorisées par le partitionnement INT-FLOAT-MEM étaient donc possibles, en plus des combinaisons INT + INT et INT + INT + MUL. Autrement dit, il pouvait faire toute ce que permettait un CPU à double émission entière, plus les combinaisons FLOAT/MUL + MEM.
Le SuperSPARC était un CPU triple émission, avec deux ALU entières, une FPU et une unité mémoire. Il pouvait émettre, à chaque cycle, trois µops en piochant dans deux opérations entières, un accès mémoire, une opération flottante. Le SuperSPARC avait de nombreuses contraintes d’appariement. Par exemple, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Une bonne partie de ces contraintes était lié au banc de registre, qui n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions.
: La seconde ALU a été abandonnée dans l'HyperSPARC, qui est de plus repassé en double émission, ce qui est une évolution à rebours de l'évolution des autres CPUs. Le SuperSPARC était en avance sur son temps, ce quelques années, alors que l'HyperSPARC a un design de son temps.
===Les processeurs historiques à quadruple émission===
C'est la même année , en 1994, que la quadruple émission est arrivée et que les CPU ont commencé à avoir de plus en plus d'unités de calcul. Même les designs restés en double émission avaient ajouté des unités de calcul. Il y avait alors trois stratégies pour la quadruple émission.
La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Les FPUs étaient soit dupliquées, soit scindées en deux en séparant l'additionneur du multiplieur flottant. Le POWER 2 dupliquait ses FPUs à la perfection, alors que l'21264 partitionnait la FPU. Les accès mémoire étaient traités comme des opérations entières, et le processeur pouvait : soit en faire un seul par cycle, soit en émettre deux, sous conditions (le cache devait être double port pour lire/écrire deux données à la fois, comme on le verra dans la suite du chapitre).
Les processeurs MIPS R8000 et R10000 allaient un peu plus loin, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités.
La seconde implémentation dupliquait l'ALU entière et combinait le tout avec un partitionnement INT-FLOAT-MEM. De quoi exécuter en même temps deux opérations entières, une opération flottante, et un accès mémoire. 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).
Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, avec au minimum deux ALU entières. Pour comprendre ce que je veux dire, prenons l'exemple de l'UltraSPARC. Il avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplier flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes. Le processeur pouvait émettre 4 instructions, tant qu'elles atterrissaient dans des unités différentes.
La même méthode peut être utilisée pour la double émission. Le Motorola 88110, sorti en 1991, était un CPU à double émission avec pas moins de 10 unités : deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise.
La morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes. Le tout est résumé dans le tableau ci-dessous, avec :
* en vert les CPU utilisant la première stratégie ;
* en rouge les CPU utilisant la seconde stratégie ;
* en jaune les CPU utilisant la troisième stratégie.
{|class="wikitable"
|+ Processeurs quadruple émission historiques
|-
! Année !! Processeur !! Unités
|-
! 1991
| class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques
|-
|-
! rowspan="2" | 1994
| class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'')
|-
| class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM
|-
! rowspan="3" | 1995
| class="f_jaune | PA 8000 || class="f_jaune | 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD
|-
| class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques
|-
| MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM
|-
! rowspan="2" | 1996
| class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM
|-
| class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL)
|}
===Les processeurs superscalaires modernes===
Les CPU superscalaires modernes ont des design légèrement différents des CPU historiques. Ils dupliquent l'unité entière, mais leur partitionnement est différent. En général, les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Par exemple, un CPU à 6 voies pourra émettre/exécuter 6 opérations entières simultanément, grâce à 6 ALU entières. Mais il s'agit là d'un maximum et certaines peuvent être remplacées par une opération flottante ou mémoire, qui utilisent l'unité mémoire et/ou la FPU.
Un exemple assez parlant est les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon. C'était une microarchitecture à triple émission, avec trois ALU entières et trois unités mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements.
Il s'agit là d'une tendance, cependant. Les CPU superscalaires tendent à mettre le paquet niveau opérations arithmétiques, mais pas forcément au point de remplir la totalité de la largeur du processeur. La règle est que pour N voies, le processeur dispose d'au moins N/2 ALU entières. Attention, cela ne signifie pas que le processeur émet systématiquement N/2 opération entières. Il peut par exemple émettre une combinaison d'opérations flottantes et mémoire, avec des branchements. Et N/2 est une borne basse, la plupart des CPU x86 ont environ tournent plutôt autour des 2/3, voire des 3/4.
En général, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. La conséquence est que les unités mémoire sont dupliquées, en seconde place dans l'ordre d'importance. Les processeurs x86 ont commencé à dupliquer leurs unités mémoire dès qu'ils ont atteint la quadruple émission, pour les CPU grand public. La microarchitecture Core d'Intel utilisait la quadruple émission, avec deux unités mémoire et trois ALU entières. Les microarchitectures d'AMD K5 et K6 utilisaient aussi la quadruple émission et avaient deux ALUs pour deux unités mémoire.
Plus récemment, la microarchitecture ''Golden Cove'' dispose de cinq unités de calcul trois unités mémoire (deux spécialisées dans les écritures, trois dans les lectures). La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire.Il y a donc soit égalité, soit avantage pour les ALU entières.
Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair.
==L'unité d'émission d'un processeur superscalaire==
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Voyons pourquoi.
===L'excès de ports d'émission===
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ou 5 ?
Une solution est de réduire la largeur du processeur. Dans l'exemple précédent, il est possible de passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La solution précédente est certes très intéressante, mais elle a des défauts. Le principal est que les ports d'émission sont sous-utilisés. Et les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
===L'intérieur de l'unité d'émission===
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Le banc de registre d'un processeur superscalaire===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Une autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions auto-aligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions auto-aligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez longs, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
swqm3gf7j6a25is6t1zv6o5h92jw96t
773158
773157
2026-09-25T19:16:11Z
Mewtow
31375
/* L'unité d'émission d'un processeur superscalaire */
773158
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===L'impact des contraintes d’appariement sur la performance===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour la suite du chapitre===
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==Le ''front-end'' des processeurs superscalaires==
Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple.
Intuitivement, charger, décoder et 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 certaines processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. 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
|}
Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs.
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
==Les processeurs implémentés avec plusieurs pipelines==
Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Vous vous dites que l'implémentation d'un processeur superscalaire n'est pas la même suivant que le CPU utilise l'exécution dans le désordre, ou non. Mais dans les faits, la distinction est plus subtile que ca. Pour l'expliquer, je vais utiliser une classification originale, qui sépare les processeurs superscalaires en trois sous-types. La distinction se fait sur la manière dont la superscalarité est implémentée en matériel.La classification que j'utilise distingue :
* Les processeurs avec plusieurs pipelines, sans exécution dans le désordre.
* Les processeurs superscalaires avec un pipeline dynamique.
* Les processeurs avec des fenêtres d'instruction décentralisées.
La distinction n'est pas qu'une question d'implémentation matérielle. Elle impacte aussi le ratio entre instructions chargées/décodées et instructions émises. Vous vous dites que les deux sont égales, mais ce n'est le cas que sur les processeurs avec N pipelines ! Sur les autres processeurs, il est possible de décoder 4 instructions, mais d'en émettre maximum 6 par cycle ! Et sur les CPU avec des fenêtres d'instruction décentralisées, il y a aussi une différence entre le nombre d'instructions qui sont ''dispatch'' et ''schedule''.
La catégorie la plus simple est celle des '''processeurs avec plusieurs pipelines'''. Ce sont des processeurs superscalaires sans exécution dans le désordre. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction.
Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, d'une FPU, éventuellement d'un second banc de registres, guère plus. Il permet d'exécuter une seconde opération de calcul, pas plus, ce qui est à l'origine de contraintes d'appariemment assez fortes. De plus, il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section.
===La double émission entière-flottante===
L'exemple le plus simple utilise un pipeline pour les opérations flottantes, séparé du pipeline originel. Pour bien expliquer les choses, prenons le processeur illustré ci-dessous. Un processeur a deux ''back-end'' séparés, un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par l'ALU entière, passent éventuellement dans l'unité mémoire, pour ensuite arriver à l'étape de ''writeback''. La FPU et le banc de registre flottant ont eux aussi leur propre ''back-end'' séparé.
[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]]
Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaqaue pipeline, à chaque cycle.
L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine.
Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants.
===La double émission entière===
Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait.
Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
==Les unités de calcul d'un processeur superscalaire==
Après avoir vu le chargement et le décodage superscalaire, voyons ce qu'il en est au niveau des unités de calcul. Nous allons parler des unités de calcul avant de parler de l'émission, pour une raison simple : l'émission fait l'interface entre unité de calcul et ''front-end'' et on ne peut pas parler de l'émission sans avoir vu comment la superscalarité impacte les deux. Si le ''front-end'' charge et décode un paquet de N instructions, son exécution demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est possible d'exploiter plusieurs unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu.
Les tout premiers CPU superscalaires étaient nombreux à utiliser cette technique. Ils étaient des CPU double émission, avec une ALU entière, une unité mémoire et une FPU. Nous parlerons de '''partitionnement INT-FLOAT-MEM'''. Il s'agit de l'HyperSPARC, de l'alpha 21064 et du PowerPC 603. Les contrainte d’appariement variaient grandement suivant le processeur. Les plus permissifs autorisaient toutes les combinaisons possibles entre : une opération entière, une opération flottante, et une unité mémoire. Mais ils n'étaient pas les seuls.
Le PowerPC 603 implémentait ce genre de double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions).
Il est aussi possible de faire la même chose pour les opérations entières, vu qu'elles sont prises en charge par trois circuits. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres. Il faut parfois lui ajouter le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible d'émettre une opération séparément, ce qui donne le '''partitionnement entier''' dans ce qui suit. Le partitionnement entier n'est jamais utilisé tel quel, car le gain en performance serait trop faible au regard des couts. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''. Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, 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]]
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications.
Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires modernes utilisent toutes ces stratégies===
Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autre unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents.
* Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU.
* La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants.
* La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables.
De nos jours, le partitionnement est utilisé en complément de la duplication des ALU entières, et de la duplication des autres unités. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993.
Le meilleur moyen de voir comment sont combinés ces quatre technique est de regarder ce qu'il s'est passé historiquement.
==L'évolution historique de la superscalarité==
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. La plupart des CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 sont rapidement montés à de la quadruple émission, avec quelques exceptions. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 ||
|-
! 1995
| 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000
|-
! 1996
| class="f_bleu | 620 || || || || class="f_bleu | 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
===Les processeurs historiques à double émission===
Pour commencer, étudions le cas des premiers CPU superscalaires à double émission. Ils sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Notamment, la duplication des autres unités ne sert pour ainsi dire à rien, ce qui limite les possibilités, mais rendra les explications plus simples.
Il pouvaient se classer en deux/trois types principaux, assez stricts, avec quelques processeurs qui ne rentraient pas dans ces cases. Le premier utilise le partitionnement seul, le second dupliquer les ALU entières sans partitionnement, et le troisième type est volontairement passé sous silence pour le moment (nous aborderons le troisième type dans la partie sur des design à quadruple émission).
Les CPU n'utilisant que le partitionnement faisaient la double émission avec trois unités : une ALU, une FPU et une unité mémoire. Si la moitié des processeurs faisait du ''partitionnement complet'' entre ALU, FPU et unité mémoire, l'autre moitié utilisait la bouble émission entière-flottante.
Pour résumer, voici le tableau chronologique disant quel CPU utilisait quelle technique.
* Le partitionnement INT-FLOAT-MEM est en jaune.
* La double émission entière-flottante est en vert.
* La double émission entière est en rouge.
* Les CPU barrés utilisent la quadruple émission et seront vu dans la section suivante.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| class="f_vert | RS/6000 || || || class="f_rouge | i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| class="f_vert | RSC || class="f_vert | PA 7100 || SuperSPARC || || class="f_jaune | 21064 ||
|-
! 1993
| class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || ||
|-
! 1994
| <s>POWER 2</s> || PA 7200 || || || <s>21164</s> ||
|}
Vous voyez que certains processeur sont laissés de côté dans le tableau précédent. C'est car ils ne rentrent pas dans les catégories précédentes et faisaient autrement. Si on omet le Motorola 88110, qui utilisait une forme de double émission assez avancée, les deux CPUs qui vont nous intéresser sont le PA-RISC PA 7200 d'HP, et le SuperSPARC.
Le PA 7200, sorti en 1994, est un PA-7100 auquel on aurait rajouté une ALU entière. Précisons que ce processeur exécutait les multiplications entières dans la FPU, comme son prédécesseur. Les combinaisons autorisées étaient donc INT + INT, INT + MEM, INT + FLOAT/MUL, MEM + FLOAT/MUL. Les combinaisons autorisées par le partitionnement INT-FLOAT-MEM étaient donc possibles, en plus des combinaisons INT + INT et INT + INT + MUL. Autrement dit, il pouvait faire toute ce que permettait un CPU à double émission entière, plus les combinaisons FLOAT/MUL + MEM.
Le SuperSPARC était un CPU triple émission, avec deux ALU entières, une FPU et une unité mémoire. Il pouvait émettre, à chaque cycle, trois µops en piochant dans deux opérations entières, un accès mémoire, une opération flottante. Le SuperSPARC avait de nombreuses contraintes d’appariement. Par exemple, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Une bonne partie de ces contraintes était lié au banc de registre, qui n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions.
: La seconde ALU a été abandonnée dans l'HyperSPARC, qui est de plus repassé en double émission, ce qui est une évolution à rebours de l'évolution des autres CPUs. Le SuperSPARC était en avance sur son temps, ce quelques années, alors que l'HyperSPARC a un design de son temps.
===Les processeurs historiques à quadruple émission===
C'est la même année , en 1994, que la quadruple émission est arrivée et que les CPU ont commencé à avoir de plus en plus d'unités de calcul. Même les designs restés en double émission avaient ajouté des unités de calcul. Il y avait alors trois stratégies pour la quadruple émission.
La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Les FPUs étaient soit dupliquées, soit scindées en deux en séparant l'additionneur du multiplieur flottant. Le POWER 2 dupliquait ses FPUs à la perfection, alors que l'21264 partitionnait la FPU. Les accès mémoire étaient traités comme des opérations entières, et le processeur pouvait : soit en faire un seul par cycle, soit en émettre deux, sous conditions (le cache devait être double port pour lire/écrire deux données à la fois, comme on le verra dans la suite du chapitre).
Les processeurs MIPS R8000 et R10000 allaient un peu plus loin, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités.
La seconde implémentation dupliquait l'ALU entière et combinait le tout avec un partitionnement INT-FLOAT-MEM. De quoi exécuter en même temps deux opérations entières, une opération flottante, et un accès mémoire. 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).
Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, avec au minimum deux ALU entières. Pour comprendre ce que je veux dire, prenons l'exemple de l'UltraSPARC. Il avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplier flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes. Le processeur pouvait émettre 4 instructions, tant qu'elles atterrissaient dans des unités différentes.
La même méthode peut être utilisée pour la double émission. Le Motorola 88110, sorti en 1991, était un CPU à double émission avec pas moins de 10 unités : deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise.
La morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes. Le tout est résumé dans le tableau ci-dessous, avec :
* en vert les CPU utilisant la première stratégie ;
* en rouge les CPU utilisant la seconde stratégie ;
* en jaune les CPU utilisant la troisième stratégie.
{|class="wikitable"
|+ Processeurs quadruple émission historiques
|-
! Année !! Processeur !! Unités
|-
! 1991
| class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques
|-
|-
! rowspan="2" | 1994
| class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'')
|-
| class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM
|-
! rowspan="3" | 1995
| class="f_jaune | PA 8000 || class="f_jaune | 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD
|-
| class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques
|-
| MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM
|-
! rowspan="2" | 1996
| class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM
|-
| class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL)
|}
===Les processeurs superscalaires modernes===
Les CPU superscalaires modernes ont des design légèrement différents des CPU historiques. Ils dupliquent l'unité entière, mais leur partitionnement est différent. En général, les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Par exemple, un CPU à 6 voies pourra émettre/exécuter 6 opérations entières simultanément, grâce à 6 ALU entières. Mais il s'agit là d'un maximum et certaines peuvent être remplacées par une opération flottante ou mémoire, qui utilisent l'unité mémoire et/ou la FPU.
Un exemple assez parlant est les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon. C'était une microarchitecture à triple émission, avec trois ALU entières et trois unités mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements.
Il s'agit là d'une tendance, cependant. Les CPU superscalaires tendent à mettre le paquet niveau opérations arithmétiques, mais pas forcément au point de remplir la totalité de la largeur du processeur. La règle est que pour N voies, le processeur dispose d'au moins N/2 ALU entières. Attention, cela ne signifie pas que le processeur émet systématiquement N/2 opération entières. Il peut par exemple émettre une combinaison d'opérations flottantes et mémoire, avec des branchements. Et N/2 est une borne basse, la plupart des CPU x86 ont environ tournent plutôt autour des 2/3, voire des 3/4.
En général, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. La conséquence est que les unités mémoire sont dupliquées, en seconde place dans l'ordre d'importance. Les processeurs x86 ont commencé à dupliquer leurs unités mémoire dès qu'ils ont atteint la quadruple émission, pour les CPU grand public. La microarchitecture Core d'Intel utilisait la quadruple émission, avec deux unités mémoire et trois ALU entières. Les microarchitectures d'AMD K5 et K6 utilisaient aussi la quadruple émission et avaient deux ALUs pour deux unités mémoire.
Plus récemment, la microarchitecture ''Golden Cove'' dispose de cinq unités de calcul trois unités mémoire (deux spécialisées dans les écritures, trois dans les lectures). La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire.Il y a donc soit égalité, soit avantage pour les ALU entières.
Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair.
==L'unité d'émission d'un processeur superscalaire==
Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Voyons pourquoi.
===L'excès de ports d'émission===
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ou 5 ?
Une solution est de réduire la largeur du processeur. Dans l'exemple précédent, il est possible de passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La solution précédente est certes très intéressante, mais elle a des défauts. Le principal est que les ports d'émission sont sous-utilisés. Et les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
===L'intérieur de l'unité d'émission===
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Le banc de registre d'un processeur superscalaire===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Une autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions auto-aligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions auto-aligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez longs, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
j6rxcsguwfk70ycr8qeivxjkmwhqjkh
773159
773158
2026-09-25T19:16:34Z
Mewtow
31375
/* Le décodage parallèle des instructions */
773159
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===L'impact des contraintes d’appariement sur la performance===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour la suite du chapitre===
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==Le ''front-end'' des processeurs superscalaires==
Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple.
Intuitivement, charger, décoder et 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 certaines processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. 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
|}
Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs.
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===L'émission parallèle des instructions===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
==Les processeurs implémentés avec plusieurs pipelines==
Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Vous vous dites que l'implémentation d'un processeur superscalaire n'est pas la même suivant que le CPU utilise l'exécution dans le désordre, ou non. Mais dans les faits, la distinction est plus subtile que ca. Pour l'expliquer, je vais utiliser une classification originale, qui sépare les processeurs superscalaires en trois sous-types. La distinction se fait sur la manière dont la superscalarité est implémentée en matériel.La classification que j'utilise distingue :
* Les processeurs avec plusieurs pipelines, sans exécution dans le désordre.
* Les processeurs superscalaires avec un pipeline dynamique.
* Les processeurs avec des fenêtres d'instruction décentralisées.
La distinction n'est pas qu'une question d'implémentation matérielle. Elle impacte aussi le ratio entre instructions chargées/décodées et instructions émises. Vous vous dites que les deux sont égales, mais ce n'est le cas que sur les processeurs avec N pipelines ! Sur les autres processeurs, il est possible de décoder 4 instructions, mais d'en émettre maximum 6 par cycle ! Et sur les CPU avec des fenêtres d'instruction décentralisées, il y a aussi une différence entre le nombre d'instructions qui sont ''dispatch'' et ''schedule''.
La catégorie la plus simple est celle des '''processeurs avec plusieurs pipelines'''. Ce sont des processeurs superscalaires sans exécution dans le désordre. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction.
Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, d'une FPU, éventuellement d'un second banc de registres, guère plus. Il permet d'exécuter une seconde opération de calcul, pas plus, ce qui est à l'origine de contraintes d'appariemment assez fortes. De plus, il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section.
===La double émission entière-flottante===
L'exemple le plus simple utilise un pipeline pour les opérations flottantes, séparé du pipeline originel. Pour bien expliquer les choses, prenons le processeur illustré ci-dessous. Un processeur a deux ''back-end'' séparés, un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par l'ALU entière, passent éventuellement dans l'unité mémoire, pour ensuite arriver à l'étape de ''writeback''. La FPU et le banc de registre flottant ont eux aussi leur propre ''back-end'' séparé.
[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]]
Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaqaue pipeline, à chaque cycle.
L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine.
Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants.
===La double émission entière===
Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait.
Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
==Les unités de calcul d'un processeur superscalaire==
Après avoir vu le chargement et le décodage superscalaire, voyons ce qu'il en est au niveau des unités de calcul. Nous allons parler des unités de calcul avant de parler de l'émission, pour une raison simple : l'émission fait l'interface entre unité de calcul et ''front-end'' et on ne peut pas parler de l'émission sans avoir vu comment la superscalarité impacte les deux. Si le ''front-end'' charge et décode un paquet de N instructions, son exécution demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est possible d'exploiter plusieurs unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu.
Les tout premiers CPU superscalaires étaient nombreux à utiliser cette technique. Ils étaient des CPU double émission, avec une ALU entière, une unité mémoire et une FPU. Nous parlerons de '''partitionnement INT-FLOAT-MEM'''. Il s'agit de l'HyperSPARC, de l'alpha 21064 et du PowerPC 603. Les contrainte d’appariement variaient grandement suivant le processeur. Les plus permissifs autorisaient toutes les combinaisons possibles entre : une opération entière, une opération flottante, et une unité mémoire. Mais ils n'étaient pas les seuls.
Le PowerPC 603 implémentait ce genre de double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions).
Il est aussi possible de faire la même chose pour les opérations entières, vu qu'elles sont prises en charge par trois circuits. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres. Il faut parfois lui ajouter le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible d'émettre une opération séparément, ce qui donne le '''partitionnement entier''' dans ce qui suit. Le partitionnement entier n'est jamais utilisé tel quel, car le gain en performance serait trop faible au regard des couts. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''. Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, 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]]
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications.
Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires modernes utilisent toutes ces stratégies===
Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autre unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents.
* Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU.
* La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants.
* La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables.
De nos jours, le partitionnement est utilisé en complément de la duplication des ALU entières, et de la duplication des autres unités. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993.
Le meilleur moyen de voir comment sont combinés ces quatre technique est de regarder ce qu'il s'est passé historiquement.
==L'évolution historique de la superscalarité==
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. La plupart des CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 sont rapidement montés à de la quadruple émission, avec quelques exceptions. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 ||
|-
! 1995
| 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000
|-
! 1996
| class="f_bleu | 620 || || || || class="f_bleu | 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
===Les processeurs historiques à double émission===
Pour commencer, étudions le cas des premiers CPU superscalaires à double émission. Ils sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Notamment, la duplication des autres unités ne sert pour ainsi dire à rien, ce qui limite les possibilités, mais rendra les explications plus simples.
Il pouvaient se classer en deux/trois types principaux, assez stricts, avec quelques processeurs qui ne rentraient pas dans ces cases. Le premier utilise le partitionnement seul, le second dupliquer les ALU entières sans partitionnement, et le troisième type est volontairement passé sous silence pour le moment (nous aborderons le troisième type dans la partie sur des design à quadruple émission).
Les CPU n'utilisant que le partitionnement faisaient la double émission avec trois unités : une ALU, une FPU et une unité mémoire. Si la moitié des processeurs faisait du ''partitionnement complet'' entre ALU, FPU et unité mémoire, l'autre moitié utilisait la bouble émission entière-flottante.
Pour résumer, voici le tableau chronologique disant quel CPU utilisait quelle technique.
* Le partitionnement INT-FLOAT-MEM est en jaune.
* La double émission entière-flottante est en vert.
* La double émission entière est en rouge.
* Les CPU barrés utilisent la quadruple émission et seront vu dans la section suivante.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| class="f_vert | RS/6000 || || || class="f_rouge | i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| class="f_vert | RSC || class="f_vert | PA 7100 || SuperSPARC || || class="f_jaune | 21064 ||
|-
! 1993
| class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || ||
|-
! 1994
| <s>POWER 2</s> || PA 7200 || || || <s>21164</s> ||
|}
Vous voyez que certains processeur sont laissés de côté dans le tableau précédent. C'est car ils ne rentrent pas dans les catégories précédentes et faisaient autrement. Si on omet le Motorola 88110, qui utilisait une forme de double émission assez avancée, les deux CPUs qui vont nous intéresser sont le PA-RISC PA 7200 d'HP, et le SuperSPARC.
Le PA 7200, sorti en 1994, est un PA-7100 auquel on aurait rajouté une ALU entière. Précisons que ce processeur exécutait les multiplications entières dans la FPU, comme son prédécesseur. Les combinaisons autorisées étaient donc INT + INT, INT + MEM, INT + FLOAT/MUL, MEM + FLOAT/MUL. Les combinaisons autorisées par le partitionnement INT-FLOAT-MEM étaient donc possibles, en plus des combinaisons INT + INT et INT + INT + MUL. Autrement dit, il pouvait faire toute ce que permettait un CPU à double émission entière, plus les combinaisons FLOAT/MUL + MEM.
Le SuperSPARC était un CPU triple émission, avec deux ALU entières, une FPU et une unité mémoire. Il pouvait émettre, à chaque cycle, trois µops en piochant dans deux opérations entières, un accès mémoire, une opération flottante. Le SuperSPARC avait de nombreuses contraintes d’appariement. Par exemple, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Une bonne partie de ces contraintes était lié au banc de registre, qui n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions.
: La seconde ALU a été abandonnée dans l'HyperSPARC, qui est de plus repassé en double émission, ce qui est une évolution à rebours de l'évolution des autres CPUs. Le SuperSPARC était en avance sur son temps, ce quelques années, alors que l'HyperSPARC a un design de son temps.
===Les processeurs historiques à quadruple émission===
C'est la même année , en 1994, que la quadruple émission est arrivée et que les CPU ont commencé à avoir de plus en plus d'unités de calcul. Même les designs restés en double émission avaient ajouté des unités de calcul. Il y avait alors trois stratégies pour la quadruple émission.
La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Les FPUs étaient soit dupliquées, soit scindées en deux en séparant l'additionneur du multiplieur flottant. Le POWER 2 dupliquait ses FPUs à la perfection, alors que l'21264 partitionnait la FPU. Les accès mémoire étaient traités comme des opérations entières, et le processeur pouvait : soit en faire un seul par cycle, soit en émettre deux, sous conditions (le cache devait être double port pour lire/écrire deux données à la fois, comme on le verra dans la suite du chapitre).
Les processeurs MIPS R8000 et R10000 allaient un peu plus loin, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités.
La seconde implémentation dupliquait l'ALU entière et combinait le tout avec un partitionnement INT-FLOAT-MEM. De quoi exécuter en même temps deux opérations entières, une opération flottante, et un accès mémoire. 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).
Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, avec au minimum deux ALU entières. Pour comprendre ce que je veux dire, prenons l'exemple de l'UltraSPARC. Il avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplier flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes. Le processeur pouvait émettre 4 instructions, tant qu'elles atterrissaient dans des unités différentes.
La même méthode peut être utilisée pour la double émission. Le Motorola 88110, sorti en 1991, était un CPU à double émission avec pas moins de 10 unités : deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise.
La morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes. Le tout est résumé dans le tableau ci-dessous, avec :
* en vert les CPU utilisant la première stratégie ;
* en rouge les CPU utilisant la seconde stratégie ;
* en jaune les CPU utilisant la troisième stratégie.
{|class="wikitable"
|+ Processeurs quadruple émission historiques
|-
! Année !! Processeur !! Unités
|-
! 1991
| class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques
|-
|-
! rowspan="2" | 1994
| class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'')
|-
| class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM
|-
! rowspan="3" | 1995
| class="f_jaune | PA 8000 || class="f_jaune | 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD
|-
| class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques
|-
| MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM
|-
! rowspan="2" | 1996
| class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM
|-
| class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL)
|}
===Les processeurs superscalaires modernes===
Les CPU superscalaires modernes ont des design légèrement différents des CPU historiques. Ils dupliquent l'unité entière, mais leur partitionnement est différent. En général, les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Par exemple, un CPU à 6 voies pourra émettre/exécuter 6 opérations entières simultanément, grâce à 6 ALU entières. Mais il s'agit là d'un maximum et certaines peuvent être remplacées par une opération flottante ou mémoire, qui utilisent l'unité mémoire et/ou la FPU.
Un exemple assez parlant est les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon. C'était une microarchitecture à triple émission, avec trois ALU entières et trois unités mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements.
Il s'agit là d'une tendance, cependant. Les CPU superscalaires tendent à mettre le paquet niveau opérations arithmétiques, mais pas forcément au point de remplir la totalité de la largeur du processeur. La règle est que pour N voies, le processeur dispose d'au moins N/2 ALU entières. Attention, cela ne signifie pas que le processeur émet systématiquement N/2 opération entières. Il peut par exemple émettre une combinaison d'opérations flottantes et mémoire, avec des branchements. Et N/2 est une borne basse, la plupart des CPU x86 ont environ tournent plutôt autour des 2/3, voire des 3/4.
En général, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. La conséquence est que les unités mémoire sont dupliquées, en seconde place dans l'ordre d'importance. Les processeurs x86 ont commencé à dupliquer leurs unités mémoire dès qu'ils ont atteint la quadruple émission, pour les CPU grand public. La microarchitecture Core d'Intel utilisait la quadruple émission, avec deux unités mémoire et trois ALU entières. Les microarchitectures d'AMD K5 et K6 utilisaient aussi la quadruple émission et avaient deux ALUs pour deux unités mémoire.
Plus récemment, la microarchitecture ''Golden Cove'' dispose de cinq unités de calcul trois unités mémoire (deux spécialisées dans les écritures, trois dans les lectures). La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire.Il y a donc soit égalité, soit avantage pour les ALU entières.
Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair.
==L'unité d'émission d'un processeur superscalaire==
Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Voyons pourquoi.
===L'excès de ports d'émission===
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ou 5 ?
Une solution est de réduire la largeur du processeur. Dans l'exemple précédent, il est possible de passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La solution précédente est certes très intéressante, mais elle a des défauts. Le principal est que les ports d'émission sont sous-utilisés. Et les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
===L'intérieur de l'unité d'émission===
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Le banc de registre d'un processeur superscalaire===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Une autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions auto-aligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions auto-aligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez longs, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
43eeuq89159ixaby570xdoxa7zcwigs
773160
773159
2026-09-25T19:19:38Z
Mewtow
31375
/* L'émission parallèle des instructions */
773160
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===L'impact des contraintes d’appariement sur la performance===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour la suite du chapitre===
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==Le ''front-end'' des processeurs superscalaires==
Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple.
Intuitivement, charger, décoder et 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 certaines processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. 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
|}
Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs.
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===L'émission parallèle des instructions===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elle sont propagée au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage).
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
==Les processeurs implémentés avec plusieurs pipelines==
Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Vous vous dites que l'implémentation d'un processeur superscalaire n'est pas la même suivant que le CPU utilise l'exécution dans le désordre, ou non. Mais dans les faits, la distinction est plus subtile que ca. Pour l'expliquer, je vais utiliser une classification originale, qui sépare les processeurs superscalaires en trois sous-types. La distinction se fait sur la manière dont la superscalarité est implémentée en matériel.La classification que j'utilise distingue :
* Les processeurs avec plusieurs pipelines, sans exécution dans le désordre.
* Les processeurs superscalaires avec un pipeline dynamique.
* Les processeurs avec des fenêtres d'instruction décentralisées.
La distinction n'est pas qu'une question d'implémentation matérielle. Elle impacte aussi le ratio entre instructions chargées/décodées et instructions émises. Vous vous dites que les deux sont égales, mais ce n'est le cas que sur les processeurs avec N pipelines ! Sur les autres processeurs, il est possible de décoder 4 instructions, mais d'en émettre maximum 6 par cycle ! Et sur les CPU avec des fenêtres d'instruction décentralisées, il y a aussi une différence entre le nombre d'instructions qui sont ''dispatch'' et ''schedule''.
La catégorie la plus simple est celle des '''processeurs avec plusieurs pipelines'''. Ce sont des processeurs superscalaires sans exécution dans le désordre. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction.
Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, d'une FPU, éventuellement d'un second banc de registres, guère plus. Il permet d'exécuter une seconde opération de calcul, pas plus, ce qui est à l'origine de contraintes d'appariemment assez fortes. De plus, il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section.
===La double émission entière-flottante===
L'exemple le plus simple utilise un pipeline pour les opérations flottantes, séparé du pipeline originel. Pour bien expliquer les choses, prenons le processeur illustré ci-dessous. Un processeur a deux ''back-end'' séparés, un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par l'ALU entière, passent éventuellement dans l'unité mémoire, pour ensuite arriver à l'étape de ''writeback''. La FPU et le banc de registre flottant ont eux aussi leur propre ''back-end'' séparé.
[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]]
Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaqaue pipeline, à chaque cycle.
L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine.
Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants.
===La double émission entière===
Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait.
Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
==Les unités de calcul d'un processeur superscalaire==
Après avoir vu le chargement et le décodage superscalaire, voyons ce qu'il en est au niveau des unités de calcul. Nous allons parler des unités de calcul avant de parler de l'émission, pour une raison simple : l'émission fait l'interface entre unité de calcul et ''front-end'' et on ne peut pas parler de l'émission sans avoir vu comment la superscalarité impacte les deux. Si le ''front-end'' charge et décode un paquet de N instructions, son exécution demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est possible d'exploiter plusieurs unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu.
Les tout premiers CPU superscalaires étaient nombreux à utiliser cette technique. Ils étaient des CPU double émission, avec une ALU entière, une unité mémoire et une FPU. Nous parlerons de '''partitionnement INT-FLOAT-MEM'''. Il s'agit de l'HyperSPARC, de l'alpha 21064 et du PowerPC 603. Les contrainte d’appariement variaient grandement suivant le processeur. Les plus permissifs autorisaient toutes les combinaisons possibles entre : une opération entière, une opération flottante, et une unité mémoire. Mais ils n'étaient pas les seuls.
Le PowerPC 603 implémentait ce genre de double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions).
Il est aussi possible de faire la même chose pour les opérations entières, vu qu'elles sont prises en charge par trois circuits. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres. Il faut parfois lui ajouter le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible d'émettre une opération séparément, ce qui donne le '''partitionnement entier''' dans ce qui suit. Le partitionnement entier n'est jamais utilisé tel quel, car le gain en performance serait trop faible au regard des couts. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''. Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, 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]]
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications.
Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires modernes utilisent toutes ces stratégies===
Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autre unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents.
* Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU.
* La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants.
* La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables.
De nos jours, le partitionnement est utilisé en complément de la duplication des ALU entières, et de la duplication des autres unités. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993.
Le meilleur moyen de voir comment sont combinés ces quatre technique est de regarder ce qu'il s'est passé historiquement.
==L'évolution historique de la superscalarité==
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. La plupart des CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 sont rapidement montés à de la quadruple émission, avec quelques exceptions. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 ||
|-
! 1995
| 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000
|-
! 1996
| class="f_bleu | 620 || || || || class="f_bleu | 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
===Les processeurs historiques à double émission===
Pour commencer, étudions le cas des premiers CPU superscalaires à double émission. Ils sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Notamment, la duplication des autres unités ne sert pour ainsi dire à rien, ce qui limite les possibilités, mais rendra les explications plus simples.
Il pouvaient se classer en deux/trois types principaux, assez stricts, avec quelques processeurs qui ne rentraient pas dans ces cases. Le premier utilise le partitionnement seul, le second dupliquer les ALU entières sans partitionnement, et le troisième type est volontairement passé sous silence pour le moment (nous aborderons le troisième type dans la partie sur des design à quadruple émission).
Les CPU n'utilisant que le partitionnement faisaient la double émission avec trois unités : une ALU, une FPU et une unité mémoire. Si la moitié des processeurs faisait du ''partitionnement complet'' entre ALU, FPU et unité mémoire, l'autre moitié utilisait la bouble émission entière-flottante.
Pour résumer, voici le tableau chronologique disant quel CPU utilisait quelle technique.
* Le partitionnement INT-FLOAT-MEM est en jaune.
* La double émission entière-flottante est en vert.
* La double émission entière est en rouge.
* Les CPU barrés utilisent la quadruple émission et seront vu dans la section suivante.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| class="f_vert | RS/6000 || || || class="f_rouge | i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| class="f_vert | RSC || class="f_vert | PA 7100 || SuperSPARC || || class="f_jaune | 21064 ||
|-
! 1993
| class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || ||
|-
! 1994
| <s>POWER 2</s> || PA 7200 || || || <s>21164</s> ||
|}
Vous voyez que certains processeur sont laissés de côté dans le tableau précédent. C'est car ils ne rentrent pas dans les catégories précédentes et faisaient autrement. Si on omet le Motorola 88110, qui utilisait une forme de double émission assez avancée, les deux CPUs qui vont nous intéresser sont le PA-RISC PA 7200 d'HP, et le SuperSPARC.
Le PA 7200, sorti en 1994, est un PA-7100 auquel on aurait rajouté une ALU entière. Précisons que ce processeur exécutait les multiplications entières dans la FPU, comme son prédécesseur. Les combinaisons autorisées étaient donc INT + INT, INT + MEM, INT + FLOAT/MUL, MEM + FLOAT/MUL. Les combinaisons autorisées par le partitionnement INT-FLOAT-MEM étaient donc possibles, en plus des combinaisons INT + INT et INT + INT + MUL. Autrement dit, il pouvait faire toute ce que permettait un CPU à double émission entière, plus les combinaisons FLOAT/MUL + MEM.
Le SuperSPARC était un CPU triple émission, avec deux ALU entières, une FPU et une unité mémoire. Il pouvait émettre, à chaque cycle, trois µops en piochant dans deux opérations entières, un accès mémoire, une opération flottante. Le SuperSPARC avait de nombreuses contraintes d’appariement. Par exemple, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Une bonne partie de ces contraintes était lié au banc de registre, qui n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions.
: La seconde ALU a été abandonnée dans l'HyperSPARC, qui est de plus repassé en double émission, ce qui est une évolution à rebours de l'évolution des autres CPUs. Le SuperSPARC était en avance sur son temps, ce quelques années, alors que l'HyperSPARC a un design de son temps.
===Les processeurs historiques à quadruple émission===
C'est la même année , en 1994, que la quadruple émission est arrivée et que les CPU ont commencé à avoir de plus en plus d'unités de calcul. Même les designs restés en double émission avaient ajouté des unités de calcul. Il y avait alors trois stratégies pour la quadruple émission.
La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Les FPUs étaient soit dupliquées, soit scindées en deux en séparant l'additionneur du multiplieur flottant. Le POWER 2 dupliquait ses FPUs à la perfection, alors que l'21264 partitionnait la FPU. Les accès mémoire étaient traités comme des opérations entières, et le processeur pouvait : soit en faire un seul par cycle, soit en émettre deux, sous conditions (le cache devait être double port pour lire/écrire deux données à la fois, comme on le verra dans la suite du chapitre).
Les processeurs MIPS R8000 et R10000 allaient un peu plus loin, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités.
La seconde implémentation dupliquait l'ALU entière et combinait le tout avec un partitionnement INT-FLOAT-MEM. De quoi exécuter en même temps deux opérations entières, une opération flottante, et un accès mémoire. 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).
Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, avec au minimum deux ALU entières. Pour comprendre ce que je veux dire, prenons l'exemple de l'UltraSPARC. Il avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplier flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes. Le processeur pouvait émettre 4 instructions, tant qu'elles atterrissaient dans des unités différentes.
La même méthode peut être utilisée pour la double émission. Le Motorola 88110, sorti en 1991, était un CPU à double émission avec pas moins de 10 unités : deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise.
La morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes. Le tout est résumé dans le tableau ci-dessous, avec :
* en vert les CPU utilisant la première stratégie ;
* en rouge les CPU utilisant la seconde stratégie ;
* en jaune les CPU utilisant la troisième stratégie.
{|class="wikitable"
|+ Processeurs quadruple émission historiques
|-
! Année !! Processeur !! Unités
|-
! 1991
| class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques
|-
|-
! rowspan="2" | 1994
| class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'')
|-
| class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM
|-
! rowspan="3" | 1995
| class="f_jaune | PA 8000 || class="f_jaune | 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD
|-
| class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques
|-
| MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM
|-
! rowspan="2" | 1996
| class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM
|-
| class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL)
|}
===Les processeurs superscalaires modernes===
Les CPU superscalaires modernes ont des design légèrement différents des CPU historiques. Ils dupliquent l'unité entière, mais leur partitionnement est différent. En général, les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Par exemple, un CPU à 6 voies pourra émettre/exécuter 6 opérations entières simultanément, grâce à 6 ALU entières. Mais il s'agit là d'un maximum et certaines peuvent être remplacées par une opération flottante ou mémoire, qui utilisent l'unité mémoire et/ou la FPU.
Un exemple assez parlant est les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon. C'était une microarchitecture à triple émission, avec trois ALU entières et trois unités mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements.
Il s'agit là d'une tendance, cependant. Les CPU superscalaires tendent à mettre le paquet niveau opérations arithmétiques, mais pas forcément au point de remplir la totalité de la largeur du processeur. La règle est que pour N voies, le processeur dispose d'au moins N/2 ALU entières. Attention, cela ne signifie pas que le processeur émet systématiquement N/2 opération entières. Il peut par exemple émettre une combinaison d'opérations flottantes et mémoire, avec des branchements. Et N/2 est une borne basse, la plupart des CPU x86 ont environ tournent plutôt autour des 2/3, voire des 3/4.
En général, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. La conséquence est que les unités mémoire sont dupliquées, en seconde place dans l'ordre d'importance. Les processeurs x86 ont commencé à dupliquer leurs unités mémoire dès qu'ils ont atteint la quadruple émission, pour les CPU grand public. La microarchitecture Core d'Intel utilisait la quadruple émission, avec deux unités mémoire et trois ALU entières. Les microarchitectures d'AMD K5 et K6 utilisaient aussi la quadruple émission et avaient deux ALUs pour deux unités mémoire.
Plus récemment, la microarchitecture ''Golden Cove'' dispose de cinq unités de calcul trois unités mémoire (deux spécialisées dans les écritures, trois dans les lectures). La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire.Il y a donc soit égalité, soit avantage pour les ALU entières.
Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair.
==L'unité d'émission d'un processeur superscalaire==
Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Voyons pourquoi.
===L'excès de ports d'émission===
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ou 5 ?
Une solution est de réduire la largeur du processeur. Dans l'exemple précédent, il est possible de passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La solution précédente est certes très intéressante, mais elle a des défauts. Le principal est que les ports d'émission sont sous-utilisés. Et les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
===L'intérieur de l'unité d'émission===
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Le banc de registre d'un processeur superscalaire===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Une autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions auto-aligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions auto-aligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez longs, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
4xqdbc84xq4l4ewowvjqqevet98u3h2
773161
773160
2026-09-25T19:21:07Z
Mewtow
31375
/* Les processeurs historiques à double émission */
773161
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===L'impact des contraintes d’appariement sur la performance===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour la suite du chapitre===
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==Le ''front-end'' des processeurs superscalaires==
Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple.
Intuitivement, charger, décoder et 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 certaines processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. 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
|}
Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs.
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===L'émission parallèle des instructions===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elle sont propagée au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage).
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
==Les processeurs implémentés avec plusieurs pipelines==
Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Vous vous dites que l'implémentation d'un processeur superscalaire n'est pas la même suivant que le CPU utilise l'exécution dans le désordre, ou non. Mais dans les faits, la distinction est plus subtile que ca. Pour l'expliquer, je vais utiliser une classification originale, qui sépare les processeurs superscalaires en trois sous-types. La distinction se fait sur la manière dont la superscalarité est implémentée en matériel.La classification que j'utilise distingue :
* Les processeurs avec plusieurs pipelines, sans exécution dans le désordre.
* Les processeurs superscalaires avec un pipeline dynamique.
* Les processeurs avec des fenêtres d'instruction décentralisées.
La distinction n'est pas qu'une question d'implémentation matérielle. Elle impacte aussi le ratio entre instructions chargées/décodées et instructions émises. Vous vous dites que les deux sont égales, mais ce n'est le cas que sur les processeurs avec N pipelines ! Sur les autres processeurs, il est possible de décoder 4 instructions, mais d'en émettre maximum 6 par cycle ! Et sur les CPU avec des fenêtres d'instruction décentralisées, il y a aussi une différence entre le nombre d'instructions qui sont ''dispatch'' et ''schedule''.
La catégorie la plus simple est celle des '''processeurs avec plusieurs pipelines'''. Ce sont des processeurs superscalaires sans exécution dans le désordre. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction.
Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, d'une FPU, éventuellement d'un second banc de registres, guère plus. Il permet d'exécuter une seconde opération de calcul, pas plus, ce qui est à l'origine de contraintes d'appariemment assez fortes. De plus, il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section.
===La double émission entière-flottante===
L'exemple le plus simple utilise un pipeline pour les opérations flottantes, séparé du pipeline originel. Pour bien expliquer les choses, prenons le processeur illustré ci-dessous. Un processeur a deux ''back-end'' séparés, un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par l'ALU entière, passent éventuellement dans l'unité mémoire, pour ensuite arriver à l'étape de ''writeback''. La FPU et le banc de registre flottant ont eux aussi leur propre ''back-end'' séparé.
[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]]
Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaqaue pipeline, à chaque cycle.
L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine.
Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants.
===La double émission entière===
Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait.
Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
==Les unités de calcul d'un processeur superscalaire==
Après avoir vu le chargement et le décodage superscalaire, voyons ce qu'il en est au niveau des unités de calcul. Nous allons parler des unités de calcul avant de parler de l'émission, pour une raison simple : l'émission fait l'interface entre unité de calcul et ''front-end'' et on ne peut pas parler de l'émission sans avoir vu comment la superscalarité impacte les deux. Si le ''front-end'' charge et décode un paquet de N instructions, son exécution demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est possible d'exploiter plusieurs unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu.
Les tout premiers CPU superscalaires étaient nombreux à utiliser cette technique. Ils étaient des CPU double émission, avec une ALU entière, une unité mémoire et une FPU. Nous parlerons de '''partitionnement INT-FLOAT-MEM'''. Il s'agit de l'HyperSPARC, de l'alpha 21064 et du PowerPC 603. Les contrainte d’appariement variaient grandement suivant le processeur. Les plus permissifs autorisaient toutes les combinaisons possibles entre : une opération entière, une opération flottante, et une unité mémoire. Mais ils n'étaient pas les seuls.
Le PowerPC 603 implémentait ce genre de double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions).
Il est aussi possible de faire la même chose pour les opérations entières, vu qu'elles sont prises en charge par trois circuits. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres. Il faut parfois lui ajouter le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible d'émettre une opération séparément, ce qui donne le '''partitionnement entier''' dans ce qui suit. Le partitionnement entier n'est jamais utilisé tel quel, car le gain en performance serait trop faible au regard des couts. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''. Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, 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]]
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications.
Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires modernes utilisent toutes ces stratégies===
Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autre unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents.
* Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU.
* La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants.
* La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables.
De nos jours, le partitionnement est utilisé en complément de la duplication des ALU entières, et de la duplication des autres unités. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993.
Le meilleur moyen de voir comment sont combinés ces quatre technique est de regarder ce qu'il s'est passé historiquement.
==L'évolution historique de la superscalarité==
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. La plupart des CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 sont rapidement montés à de la quadruple émission, avec quelques exceptions. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 ||
|-
! 1995
| 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000
|-
! 1996
| class="f_bleu | 620 || || || || class="f_bleu | 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
===Les processeurs historiques à double émission===
Pour commencer, étudions le cas des premiers CPU superscalaires à double émission. Ils sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Notamment, la duplication des autres unités ne sert pour ainsi dire à rien, ce qui limite les possibilités, mais rendra les explications plus simples.
Il pouvaient se classer en trois types principaux, assez stricts, avec quelques processeurs qui ne rentraient pas dans ces cases. Le premier utilise un ''partitionnement complet'' entre ALU, FPU et unité mémoire. Le second utilisait la double émission entière-flottante. Le troisième utilisait la double émission entière. Voici le tableau chronologique disant quel CPU utilisait quelle technique.
* Le partitionnement INT-FLOAT-MEM est en jaune.
* La double émission entière-flottante est en vert.
* La double émission entière est en rouge.
* Les CPU barrés utilisent la quadruple émission et seront vu dans la section suivante.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| class="f_vert | RS/6000 || || || class="f_rouge | i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| class="f_vert | RSC || class="f_vert | PA 7100 || SuperSPARC || || class="f_jaune | 21064 ||
|-
! 1993
| class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || ||
|-
! 1994
| <s>POWER 2</s> || PA 7200 || || || <s>21164</s> ||
|}
Vous voyez que certains processeur sont laissés de côté dans le tableau précédent. C'est car ils ne rentrent pas dans les catégories précédentes et faisaient autrement. Si on omet le Motorola 88110, qui utilisait une forme de double émission assez avancée, les deux CPUs qui vont nous intéresser sont le PA-RISC PA 7200 d'HP, et le SuperSPARC.
Le PA 7200, sorti en 1994, est un PA-7100 auquel on aurait rajouté une ALU entière. Précisons que ce processeur exécutait les multiplications entières dans la FPU, comme son prédécesseur. Les combinaisons autorisées étaient donc INT + INT, INT + MEM, INT + FLOAT/MUL, MEM + FLOAT/MUL. Les combinaisons autorisées par le partitionnement INT-FLOAT-MEM étaient donc possibles, en plus des combinaisons INT + INT et INT + INT + MUL. Autrement dit, il pouvait faire toute ce que permettait un CPU à double émission entière, plus les combinaisons FLOAT/MUL + MEM.
Le SuperSPARC était un CPU triple émission, avec deux ALU entières, une FPU et une unité mémoire. Il pouvait émettre, à chaque cycle, trois µops en piochant dans deux opérations entières, un accès mémoire, une opération flottante. Le SuperSPARC avait de nombreuses contraintes d’appariement. Par exemple, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Une bonne partie de ces contraintes était lié au banc de registre, qui n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions.
: La seconde ALU a été abandonnée dans l'HyperSPARC, qui est de plus repassé en double émission, ce qui est une évolution à rebours de l'évolution des autres CPUs. Le SuperSPARC était en avance sur son temps, ce quelques années, alors que l'HyperSPARC a un design de son temps.
===Les processeurs historiques à quadruple émission===
C'est la même année , en 1994, que la quadruple émission est arrivée et que les CPU ont commencé à avoir de plus en plus d'unités de calcul. Même les designs restés en double émission avaient ajouté des unités de calcul. Il y avait alors trois stratégies pour la quadruple émission.
La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Les FPUs étaient soit dupliquées, soit scindées en deux en séparant l'additionneur du multiplieur flottant. Le POWER 2 dupliquait ses FPUs à la perfection, alors que l'21264 partitionnait la FPU. Les accès mémoire étaient traités comme des opérations entières, et le processeur pouvait : soit en faire un seul par cycle, soit en émettre deux, sous conditions (le cache devait être double port pour lire/écrire deux données à la fois, comme on le verra dans la suite du chapitre).
Les processeurs MIPS R8000 et R10000 allaient un peu plus loin, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités.
La seconde implémentation dupliquait l'ALU entière et combinait le tout avec un partitionnement INT-FLOAT-MEM. De quoi exécuter en même temps deux opérations entières, une opération flottante, et un accès mémoire. 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).
Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, avec au minimum deux ALU entières. Pour comprendre ce que je veux dire, prenons l'exemple de l'UltraSPARC. Il avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplier flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes. Le processeur pouvait émettre 4 instructions, tant qu'elles atterrissaient dans des unités différentes.
La même méthode peut être utilisée pour la double émission. Le Motorola 88110, sorti en 1991, était un CPU à double émission avec pas moins de 10 unités : deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise.
La morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes. Le tout est résumé dans le tableau ci-dessous, avec :
* en vert les CPU utilisant la première stratégie ;
* en rouge les CPU utilisant la seconde stratégie ;
* en jaune les CPU utilisant la troisième stratégie.
{|class="wikitable"
|+ Processeurs quadruple émission historiques
|-
! Année !! Processeur !! Unités
|-
! 1991
| class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques
|-
|-
! rowspan="2" | 1994
| class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'')
|-
| class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM
|-
! rowspan="3" | 1995
| class="f_jaune | PA 8000 || class="f_jaune | 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD
|-
| class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques
|-
| MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM
|-
! rowspan="2" | 1996
| class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM
|-
| class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL)
|}
===Les processeurs superscalaires modernes===
Les CPU superscalaires modernes ont des design légèrement différents des CPU historiques. Ils dupliquent l'unité entière, mais leur partitionnement est différent. En général, les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Par exemple, un CPU à 6 voies pourra émettre/exécuter 6 opérations entières simultanément, grâce à 6 ALU entières. Mais il s'agit là d'un maximum et certaines peuvent être remplacées par une opération flottante ou mémoire, qui utilisent l'unité mémoire et/ou la FPU.
Un exemple assez parlant est les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon. C'était une microarchitecture à triple émission, avec trois ALU entières et trois unités mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements.
Il s'agit là d'une tendance, cependant. Les CPU superscalaires tendent à mettre le paquet niveau opérations arithmétiques, mais pas forcément au point de remplir la totalité de la largeur du processeur. La règle est que pour N voies, le processeur dispose d'au moins N/2 ALU entières. Attention, cela ne signifie pas que le processeur émet systématiquement N/2 opération entières. Il peut par exemple émettre une combinaison d'opérations flottantes et mémoire, avec des branchements. Et N/2 est une borne basse, la plupart des CPU x86 ont environ tournent plutôt autour des 2/3, voire des 3/4.
En général, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. La conséquence est que les unités mémoire sont dupliquées, en seconde place dans l'ordre d'importance. Les processeurs x86 ont commencé à dupliquer leurs unités mémoire dès qu'ils ont atteint la quadruple émission, pour les CPU grand public. La microarchitecture Core d'Intel utilisait la quadruple émission, avec deux unités mémoire et trois ALU entières. Les microarchitectures d'AMD K5 et K6 utilisaient aussi la quadruple émission et avaient deux ALUs pour deux unités mémoire.
Plus récemment, la microarchitecture ''Golden Cove'' dispose de cinq unités de calcul trois unités mémoire (deux spécialisées dans les écritures, trois dans les lectures). La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire.Il y a donc soit égalité, soit avantage pour les ALU entières.
Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair.
==L'unité d'émission d'un processeur superscalaire==
Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Voyons pourquoi.
===L'excès de ports d'émission===
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ou 5 ?
Une solution est de réduire la largeur du processeur. Dans l'exemple précédent, il est possible de passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La solution précédente est certes très intéressante, mais elle a des défauts. Le principal est que les ports d'émission sont sous-utilisés. Et les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
===L'intérieur de l'unité d'émission===
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Le banc de registre d'un processeur superscalaire===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Une autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions auto-aligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions auto-aligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez longs, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
820l5fr2lh2zubc55r05cu0tu94ii4u
773162
773161
2026-09-25T19:21:28Z
Mewtow
31375
/* L'unité d'émission d'un processeur superscalaire */
773162
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===L'impact des contraintes d’appariement sur la performance===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour la suite du chapitre===
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==Le ''front-end'' des processeurs superscalaires==
Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple.
Intuitivement, charger, décoder et 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 certaines processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. 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
|}
Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs.
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===L'émission parallèle des instructions===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elle sont propagée au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage).
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
==Les processeurs implémentés avec plusieurs pipelines==
Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Vous vous dites que l'implémentation d'un processeur superscalaire n'est pas la même suivant que le CPU utilise l'exécution dans le désordre, ou non. Mais dans les faits, la distinction est plus subtile que ca. Pour l'expliquer, je vais utiliser une classification originale, qui sépare les processeurs superscalaires en trois sous-types. La distinction se fait sur la manière dont la superscalarité est implémentée en matériel.La classification que j'utilise distingue :
* Les processeurs avec plusieurs pipelines, sans exécution dans le désordre.
* Les processeurs superscalaires avec un pipeline dynamique.
* Les processeurs avec des fenêtres d'instruction décentralisées.
La distinction n'est pas qu'une question d'implémentation matérielle. Elle impacte aussi le ratio entre instructions chargées/décodées et instructions émises. Vous vous dites que les deux sont égales, mais ce n'est le cas que sur les processeurs avec N pipelines ! Sur les autres processeurs, il est possible de décoder 4 instructions, mais d'en émettre maximum 6 par cycle ! Et sur les CPU avec des fenêtres d'instruction décentralisées, il y a aussi une différence entre le nombre d'instructions qui sont ''dispatch'' et ''schedule''.
La catégorie la plus simple est celle des '''processeurs avec plusieurs pipelines'''. Ce sont des processeurs superscalaires sans exécution dans le désordre. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction.
Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, d'une FPU, éventuellement d'un second banc de registres, guère plus. Il permet d'exécuter une seconde opération de calcul, pas plus, ce qui est à l'origine de contraintes d'appariemment assez fortes. De plus, il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section.
===La double émission entière-flottante===
L'exemple le plus simple utilise un pipeline pour les opérations flottantes, séparé du pipeline originel. Pour bien expliquer les choses, prenons le processeur illustré ci-dessous. Un processeur a deux ''back-end'' séparés, un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par l'ALU entière, passent éventuellement dans l'unité mémoire, pour ensuite arriver à l'étape de ''writeback''. La FPU et le banc de registre flottant ont eux aussi leur propre ''back-end'' séparé.
[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]]
Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaqaue pipeline, à chaque cycle.
L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine.
Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants.
===La double émission entière===
Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait.
Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
==Les unités de calcul d'un processeur superscalaire==
Après avoir vu le chargement et le décodage superscalaire, voyons ce qu'il en est au niveau des unités de calcul. Nous allons parler des unités de calcul avant de parler de l'émission, pour une raison simple : l'émission fait l'interface entre unité de calcul et ''front-end'' et on ne peut pas parler de l'émission sans avoir vu comment la superscalarité impacte les deux. Si le ''front-end'' charge et décode un paquet de N instructions, son exécution demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est possible d'exploiter plusieurs unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu.
Les tout premiers CPU superscalaires étaient nombreux à utiliser cette technique. Ils étaient des CPU double émission, avec une ALU entière, une unité mémoire et une FPU. Nous parlerons de '''partitionnement INT-FLOAT-MEM'''. Il s'agit de l'HyperSPARC, de l'alpha 21064 et du PowerPC 603. Les contrainte d’appariement variaient grandement suivant le processeur. Les plus permissifs autorisaient toutes les combinaisons possibles entre : une opération entière, une opération flottante, et une unité mémoire. Mais ils n'étaient pas les seuls.
Le PowerPC 603 implémentait ce genre de double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions).
Il est aussi possible de faire la même chose pour les opérations entières, vu qu'elles sont prises en charge par trois circuits. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres. Il faut parfois lui ajouter le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible d'émettre une opération séparément, ce qui donne le '''partitionnement entier''' dans ce qui suit. Le partitionnement entier n'est jamais utilisé tel quel, car le gain en performance serait trop faible au regard des couts. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''. Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, 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]]
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications.
Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires modernes utilisent toutes ces stratégies===
Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autre unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents.
* Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU.
* La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants.
* La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables.
De nos jours, le partitionnement est utilisé en complément de la duplication des ALU entières, et de la duplication des autres unités. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993.
Le meilleur moyen de voir comment sont combinés ces quatre technique est de regarder ce qu'il s'est passé historiquement.
==L'évolution historique de la superscalarité==
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. La plupart des CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 sont rapidement montés à de la quadruple émission, avec quelques exceptions. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 ||
|-
! 1995
| 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000
|-
! 1996
| class="f_bleu | 620 || || || || class="f_bleu | 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
===Les processeurs historiques à double émission===
Pour commencer, étudions le cas des premiers CPU superscalaires à double émission. Ils sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Notamment, la duplication des autres unités ne sert pour ainsi dire à rien, ce qui limite les possibilités, mais rendra les explications plus simples.
Il pouvaient se classer en trois types principaux, assez stricts, avec quelques processeurs qui ne rentraient pas dans ces cases. Le premier utilise un ''partitionnement complet'' entre ALU, FPU et unité mémoire. Le second utilisait la double émission entière-flottante. Le troisième utilisait la double émission entière. Voici le tableau chronologique disant quel CPU utilisait quelle technique.
* Le partitionnement INT-FLOAT-MEM est en jaune.
* La double émission entière-flottante est en vert.
* La double émission entière est en rouge.
* Les CPU barrés utilisent la quadruple émission et seront vu dans la section suivante.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| class="f_vert | RS/6000 || || || class="f_rouge | i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| class="f_vert | RSC || class="f_vert | PA 7100 || SuperSPARC || || class="f_jaune | 21064 ||
|-
! 1993
| class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || ||
|-
! 1994
| <s>POWER 2</s> || PA 7200 || || || <s>21164</s> ||
|}
Vous voyez que certains processeur sont laissés de côté dans le tableau précédent. C'est car ils ne rentrent pas dans les catégories précédentes et faisaient autrement. Si on omet le Motorola 88110, qui utilisait une forme de double émission assez avancée, les deux CPUs qui vont nous intéresser sont le PA-RISC PA 7200 d'HP, et le SuperSPARC.
Le PA 7200, sorti en 1994, est un PA-7100 auquel on aurait rajouté une ALU entière. Précisons que ce processeur exécutait les multiplications entières dans la FPU, comme son prédécesseur. Les combinaisons autorisées étaient donc INT + INT, INT + MEM, INT + FLOAT/MUL, MEM + FLOAT/MUL. Les combinaisons autorisées par le partitionnement INT-FLOAT-MEM étaient donc possibles, en plus des combinaisons INT + INT et INT + INT + MUL. Autrement dit, il pouvait faire toute ce que permettait un CPU à double émission entière, plus les combinaisons FLOAT/MUL + MEM.
Le SuperSPARC était un CPU triple émission, avec deux ALU entières, une FPU et une unité mémoire. Il pouvait émettre, à chaque cycle, trois µops en piochant dans deux opérations entières, un accès mémoire, une opération flottante. Le SuperSPARC avait de nombreuses contraintes d’appariement. Par exemple, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Une bonne partie de ces contraintes était lié au banc de registre, qui n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions.
: La seconde ALU a été abandonnée dans l'HyperSPARC, qui est de plus repassé en double émission, ce qui est une évolution à rebours de l'évolution des autres CPUs. Le SuperSPARC était en avance sur son temps, ce quelques années, alors que l'HyperSPARC a un design de son temps.
===Les processeurs historiques à quadruple émission===
C'est la même année , en 1994, que la quadruple émission est arrivée et que les CPU ont commencé à avoir de plus en plus d'unités de calcul. Même les designs restés en double émission avaient ajouté des unités de calcul. Il y avait alors trois stratégies pour la quadruple émission.
La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Les FPUs étaient soit dupliquées, soit scindées en deux en séparant l'additionneur du multiplieur flottant. Le POWER 2 dupliquait ses FPUs à la perfection, alors que l'21264 partitionnait la FPU. Les accès mémoire étaient traités comme des opérations entières, et le processeur pouvait : soit en faire un seul par cycle, soit en émettre deux, sous conditions (le cache devait être double port pour lire/écrire deux données à la fois, comme on le verra dans la suite du chapitre).
Les processeurs MIPS R8000 et R10000 allaient un peu plus loin, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités.
La seconde implémentation dupliquait l'ALU entière et combinait le tout avec un partitionnement INT-FLOAT-MEM. De quoi exécuter en même temps deux opérations entières, une opération flottante, et un accès mémoire. 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).
Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, avec au minimum deux ALU entières. Pour comprendre ce que je veux dire, prenons l'exemple de l'UltraSPARC. Il avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplier flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes. Le processeur pouvait émettre 4 instructions, tant qu'elles atterrissaient dans des unités différentes.
La même méthode peut être utilisée pour la double émission. Le Motorola 88110, sorti en 1991, était un CPU à double émission avec pas moins de 10 unités : deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise.
La morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes. Le tout est résumé dans le tableau ci-dessous, avec :
* en vert les CPU utilisant la première stratégie ;
* en rouge les CPU utilisant la seconde stratégie ;
* en jaune les CPU utilisant la troisième stratégie.
{|class="wikitable"
|+ Processeurs quadruple émission historiques
|-
! Année !! Processeur !! Unités
|-
! 1991
| class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques
|-
|-
! rowspan="2" | 1994
| class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'')
|-
| class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM
|-
! rowspan="3" | 1995
| class="f_jaune | PA 8000 || class="f_jaune | 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD
|-
| class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques
|-
| MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM
|-
! rowspan="2" | 1996
| class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM
|-
| class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL)
|}
===Les processeurs superscalaires modernes===
Les CPU superscalaires modernes ont des design légèrement différents des CPU historiques. Ils dupliquent l'unité entière, mais leur partitionnement est différent. En général, les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Par exemple, un CPU à 6 voies pourra émettre/exécuter 6 opérations entières simultanément, grâce à 6 ALU entières. Mais il s'agit là d'un maximum et certaines peuvent être remplacées par une opération flottante ou mémoire, qui utilisent l'unité mémoire et/ou la FPU.
Un exemple assez parlant est les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon. C'était une microarchitecture à triple émission, avec trois ALU entières et trois unités mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements.
Il s'agit là d'une tendance, cependant. Les CPU superscalaires tendent à mettre le paquet niveau opérations arithmétiques, mais pas forcément au point de remplir la totalité de la largeur du processeur. La règle est que pour N voies, le processeur dispose d'au moins N/2 ALU entières. Attention, cela ne signifie pas que le processeur émet systématiquement N/2 opération entières. Il peut par exemple émettre une combinaison d'opérations flottantes et mémoire, avec des branchements. Et N/2 est une borne basse, la plupart des CPU x86 ont environ tournent plutôt autour des 2/3, voire des 3/4.
En général, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. La conséquence est que les unités mémoire sont dupliquées, en seconde place dans l'ordre d'importance. Les processeurs x86 ont commencé à dupliquer leurs unités mémoire dès qu'ils ont atteint la quadruple émission, pour les CPU grand public. La microarchitecture Core d'Intel utilisait la quadruple émission, avec deux unités mémoire et trois ALU entières. Les microarchitectures d'AMD K5 et K6 utilisaient aussi la quadruple émission et avaient deux ALUs pour deux unités mémoire.
Plus récemment, la microarchitecture ''Golden Cove'' dispose de cinq unités de calcul trois unités mémoire (deux spécialisées dans les écritures, trois dans les lectures). La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire.Il y a donc soit égalité, soit avantage pour les ALU entières.
Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair.
==L'unité d'émission d'un processeur superscalaire==
Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Voyons pourquoi.
===L'excès de ports d'émission===
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ou 5 ?
Une solution est de réduire la largeur du processeur. Dans l'exemple précédent, il est possible de passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La solution précédente est certes très intéressante, mais elle a des défauts. Le principal est que les ports d'émission sont sous-utilisés. Et les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
===L'intérieur de l'unité d'émission===
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Le banc de registre d'un processeur superscalaire===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Une autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions auto-aligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions auto-aligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez longs, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
o4x1m95x5fdf61c70f09xq964ullly4
773163
773162
2026-09-25T19:26:20Z
Mewtow
31375
/* L'unité d'émission d'un processeur superscalaire */
773163
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===L'impact des contraintes d’appariement sur la performance===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour la suite du chapitre===
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==Le ''front-end'' des processeurs superscalaires==
Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple.
Intuitivement, charger, décoder et 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 certaines processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. 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
|}
Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs.
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===L'émission parallèle des instructions===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elle sont propagée au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage).
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
==Les processeurs implémentés avec plusieurs pipelines==
Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Vous vous dites que l'implémentation d'un processeur superscalaire n'est pas la même suivant que le CPU utilise l'exécution dans le désordre, ou non. Mais dans les faits, la distinction est plus subtile que ca. Pour l'expliquer, je vais utiliser une classification originale, qui sépare les processeurs superscalaires en trois sous-types. La distinction se fait sur la manière dont la superscalarité est implémentée en matériel.La classification que j'utilise distingue :
* Les processeurs avec plusieurs pipelines, sans exécution dans le désordre.
* Les processeurs superscalaires avec un pipeline dynamique.
* Les processeurs avec des fenêtres d'instruction décentralisées.
La distinction n'est pas qu'une question d'implémentation matérielle. Elle impacte aussi le ratio entre instructions chargées/décodées et instructions émises. Vous vous dites que les deux sont égales, mais ce n'est le cas que sur les processeurs avec N pipelines ! Sur les autres processeurs, il est possible de décoder 4 instructions, mais d'en émettre maximum 6 par cycle ! Et sur les CPU avec des fenêtres d'instruction décentralisées, il y a aussi une différence entre le nombre d'instructions qui sont ''dispatch'' et ''schedule''.
La catégorie la plus simple est celle des '''processeurs avec plusieurs pipelines'''. Ce sont des processeurs superscalaires sans exécution dans le désordre. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction.
Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, d'une FPU, éventuellement d'un second banc de registres, guère plus. Il permet d'exécuter une seconde opération de calcul, pas plus, ce qui est à l'origine de contraintes d'appariemment assez fortes. De plus, il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section.
===La double émission entière-flottante===
L'exemple le plus simple utilise un pipeline pour les opérations flottantes, séparé du pipeline originel. Pour bien expliquer les choses, prenons le processeur illustré ci-dessous. Un processeur a deux ''back-end'' séparés, un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par l'ALU entière, passent éventuellement dans l'unité mémoire, pour ensuite arriver à l'étape de ''writeback''. La FPU et le banc de registre flottant ont eux aussi leur propre ''back-end'' séparé.
[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]]
Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaqaue pipeline, à chaque cycle.
L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine.
Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants.
===La double émission entière===
Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait.
Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
==Les unités de calcul d'un processeur superscalaire==
Après avoir vu le chargement et le décodage superscalaire, voyons ce qu'il en est au niveau des unités de calcul. Nous allons parler des unités de calcul avant de parler de l'émission, pour une raison simple : l'émission fait l'interface entre unité de calcul et ''front-end'' et on ne peut pas parler de l'émission sans avoir vu comment la superscalarité impacte les deux. Si le ''front-end'' charge et décode un paquet de N instructions, son exécution demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est possible d'exploiter plusieurs unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu.
Les tout premiers CPU superscalaires étaient nombreux à utiliser cette technique. Ils étaient des CPU double émission, avec une ALU entière, une unité mémoire et une FPU. Nous parlerons de '''partitionnement INT-FLOAT-MEM'''. Il s'agit de l'HyperSPARC, de l'alpha 21064 et du PowerPC 603. Les contrainte d’appariement variaient grandement suivant le processeur. Les plus permissifs autorisaient toutes les combinaisons possibles entre : une opération entière, une opération flottante, et une unité mémoire. Mais ils n'étaient pas les seuls.
Le PowerPC 603 implémentait ce genre de double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions).
Il est aussi possible de faire la même chose pour les opérations entières, vu qu'elles sont prises en charge par trois circuits. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres. Il faut parfois lui ajouter le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible d'émettre une opération séparément, ce qui donne le '''partitionnement entier''' dans ce qui suit. Le partitionnement entier n'est jamais utilisé tel quel, car le gain en performance serait trop faible au regard des couts. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''. Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, 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]]
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications.
Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires modernes utilisent toutes ces stratégies===
Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autre unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents.
* Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU.
* La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants.
* La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables.
De nos jours, le partitionnement est utilisé en complément de la duplication des ALU entières, et de la duplication des autres unités. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993.
Le meilleur moyen de voir comment sont combinés ces quatre technique est de regarder ce qu'il s'est passé historiquement.
==L'évolution historique de la superscalarité==
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. La plupart des CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 sont rapidement montés à de la quadruple émission, avec quelques exceptions. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 ||
|-
! 1995
| 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000
|-
! 1996
| class="f_bleu | 620 || || || || class="f_bleu | 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
===Les processeurs historiques à double émission===
Pour commencer, étudions le cas des premiers CPU superscalaires à double émission. Ils sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Notamment, la duplication des autres unités ne sert pour ainsi dire à rien, ce qui limite les possibilités, mais rendra les explications plus simples.
Il pouvaient se classer en trois types principaux, assez stricts, avec quelques processeurs qui ne rentraient pas dans ces cases. Le premier utilise un ''partitionnement complet'' entre ALU, FPU et unité mémoire. Le second utilisait la double émission entière-flottante. Le troisième utilisait la double émission entière. Voici le tableau chronologique disant quel CPU utilisait quelle technique.
* Le partitionnement INT-FLOAT-MEM est en jaune.
* La double émission entière-flottante est en vert.
* La double émission entière est en rouge.
* Les CPU barrés utilisent la quadruple émission et seront vu dans la section suivante.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| class="f_vert | RS/6000 || || || class="f_rouge | i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| class="f_vert | RSC || class="f_vert | PA 7100 || SuperSPARC || || class="f_jaune | 21064 ||
|-
! 1993
| class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || ||
|-
! 1994
| <s>POWER 2</s> || PA 7200 || || || <s>21164</s> ||
|}
Vous voyez que certains processeur sont laissés de côté dans le tableau précédent. C'est car ils ne rentrent pas dans les catégories précédentes et faisaient autrement. Si on omet le Motorola 88110, qui utilisait une forme de double émission assez avancée, les deux CPUs qui vont nous intéresser sont le PA-RISC PA 7200 d'HP, et le SuperSPARC.
Le PA 7200, sorti en 1994, est un PA-7100 auquel on aurait rajouté une ALU entière. Précisons que ce processeur exécutait les multiplications entières dans la FPU, comme son prédécesseur. Les combinaisons autorisées étaient donc INT + INT, INT + MEM, INT + FLOAT/MUL, MEM + FLOAT/MUL. Les combinaisons autorisées par le partitionnement INT-FLOAT-MEM étaient donc possibles, en plus des combinaisons INT + INT et INT + INT + MUL. Autrement dit, il pouvait faire toute ce que permettait un CPU à double émission entière, plus les combinaisons FLOAT/MUL + MEM.
Le SuperSPARC était un CPU triple émission, avec deux ALU entières, une FPU et une unité mémoire. Il pouvait émettre, à chaque cycle, trois µops en piochant dans deux opérations entières, un accès mémoire, une opération flottante. Le SuperSPARC avait de nombreuses contraintes d’appariement. Par exemple, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Une bonne partie de ces contraintes était lié au banc de registre, qui n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions.
: La seconde ALU a été abandonnée dans l'HyperSPARC, qui est de plus repassé en double émission, ce qui est une évolution à rebours de l'évolution des autres CPUs. Le SuperSPARC était en avance sur son temps, ce quelques années, alors que l'HyperSPARC a un design de son temps.
===Les processeurs historiques à quadruple émission===
C'est la même année , en 1994, que la quadruple émission est arrivée et que les CPU ont commencé à avoir de plus en plus d'unités de calcul. Même les designs restés en double émission avaient ajouté des unités de calcul. Il y avait alors trois stratégies pour la quadruple émission.
La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Les FPUs étaient soit dupliquées, soit scindées en deux en séparant l'additionneur du multiplieur flottant. Le POWER 2 dupliquait ses FPUs à la perfection, alors que l'21264 partitionnait la FPU. Les accès mémoire étaient traités comme des opérations entières, et le processeur pouvait : soit en faire un seul par cycle, soit en émettre deux, sous conditions (le cache devait être double port pour lire/écrire deux données à la fois, comme on le verra dans la suite du chapitre).
Les processeurs MIPS R8000 et R10000 allaient un peu plus loin, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités.
La seconde implémentation dupliquait l'ALU entière et combinait le tout avec un partitionnement INT-FLOAT-MEM. De quoi exécuter en même temps deux opérations entières, une opération flottante, et un accès mémoire. 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).
Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, avec au minimum deux ALU entières. Pour comprendre ce que je veux dire, prenons l'exemple de l'UltraSPARC. Il avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplier flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes. Le processeur pouvait émettre 4 instructions, tant qu'elles atterrissaient dans des unités différentes.
La même méthode peut être utilisée pour la double émission. Le Motorola 88110, sorti en 1991, était un CPU à double émission avec pas moins de 10 unités : deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise.
La morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes. Le tout est résumé dans le tableau ci-dessous, avec :
* en vert les CPU utilisant la première stratégie ;
* en rouge les CPU utilisant la seconde stratégie ;
* en jaune les CPU utilisant la troisième stratégie.
{|class="wikitable"
|+ Processeurs quadruple émission historiques
|-
! Année !! Processeur !! Unités
|-
! 1991
| class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques
|-
|-
! rowspan="2" | 1994
| class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'')
|-
| class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM
|-
! rowspan="3" | 1995
| class="f_jaune | PA 8000 || class="f_jaune | 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD
|-
| class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques
|-
| MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM
|-
! rowspan="2" | 1996
| class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM
|-
| class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL)
|}
===Les processeurs superscalaires modernes===
Les CPU superscalaires modernes ont des design légèrement différents des CPU historiques. Ils dupliquent l'unité entière, mais leur partitionnement est différent. En général, les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Par exemple, un CPU à 6 voies pourra émettre/exécuter 6 opérations entières simultanément, grâce à 6 ALU entières. Mais il s'agit là d'un maximum et certaines peuvent être remplacées par une opération flottante ou mémoire, qui utilisent l'unité mémoire et/ou la FPU.
Un exemple assez parlant est les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon. C'était une microarchitecture à triple émission, avec trois ALU entières et trois unités mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements.
Il s'agit là d'une tendance, cependant. Les CPU superscalaires tendent à mettre le paquet niveau opérations arithmétiques, mais pas forcément au point de remplir la totalité de la largeur du processeur. La règle est que pour N voies, le processeur dispose d'au moins N/2 ALU entières. Attention, cela ne signifie pas que le processeur émet systématiquement N/2 opération entières. Il peut par exemple émettre une combinaison d'opérations flottantes et mémoire, avec des branchements. Et N/2 est une borne basse, la plupart des CPU x86 ont environ tournent plutôt autour des 2/3, voire des 3/4.
En général, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. La conséquence est que les unités mémoire sont dupliquées, en seconde place dans l'ordre d'importance. Les processeurs x86 ont commencé à dupliquer leurs unités mémoire dès qu'ils ont atteint la quadruple émission, pour les CPU grand public. La microarchitecture Core d'Intel utilisait la quadruple émission, avec deux unités mémoire et trois ALU entières. Les microarchitectures d'AMD K5 et K6 utilisaient aussi la quadruple émission et avaient deux ALUs pour deux unités mémoire.
Plus récemment, la microarchitecture ''Golden Cove'' dispose de cinq unités de calcul trois unités mémoire (deux spécialisées dans les écritures, trois dans les lectures). La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire.Il y a donc soit égalité, soit avantage pour les ALU entières.
Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Le banc de registre d'un processeur superscalaire===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Une autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions auto-aligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions auto-aligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez longs, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
88yak5xz1ha34fd2cz0xkhu9yorxe5w
773164
773163
2026-09-25T19:26:32Z
Mewtow
31375
/* Les CPU superscalaires modernes utilisent toutes ces stratégies */
773164
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===L'impact des contraintes d’appariement sur la performance===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour la suite du chapitre===
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==Le ''front-end'' des processeurs superscalaires==
Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple.
Intuitivement, charger, décoder et 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 certaines processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. 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
|}
Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs.
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===L'émission parallèle des instructions===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elle sont propagée au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage).
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
==Les processeurs implémentés avec plusieurs pipelines==
Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Vous vous dites que l'implémentation d'un processeur superscalaire n'est pas la même suivant que le CPU utilise l'exécution dans le désordre, ou non. Mais dans les faits, la distinction est plus subtile que ca. Pour l'expliquer, je vais utiliser une classification originale, qui sépare les processeurs superscalaires en trois sous-types. La distinction se fait sur la manière dont la superscalarité est implémentée en matériel.La classification que j'utilise distingue :
* Les processeurs avec plusieurs pipelines, sans exécution dans le désordre.
* Les processeurs superscalaires avec un pipeline dynamique.
* Les processeurs avec des fenêtres d'instruction décentralisées.
La distinction n'est pas qu'une question d'implémentation matérielle. Elle impacte aussi le ratio entre instructions chargées/décodées et instructions émises. Vous vous dites que les deux sont égales, mais ce n'est le cas que sur les processeurs avec N pipelines ! Sur les autres processeurs, il est possible de décoder 4 instructions, mais d'en émettre maximum 6 par cycle ! Et sur les CPU avec des fenêtres d'instruction décentralisées, il y a aussi une différence entre le nombre d'instructions qui sont ''dispatch'' et ''schedule''.
La catégorie la plus simple est celle des '''processeurs avec plusieurs pipelines'''. Ce sont des processeurs superscalaires sans exécution dans le désordre. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction.
Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, d'une FPU, éventuellement d'un second banc de registres, guère plus. Il permet d'exécuter une seconde opération de calcul, pas plus, ce qui est à l'origine de contraintes d'appariemment assez fortes. De plus, il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section.
===La double émission entière-flottante===
L'exemple le plus simple utilise un pipeline pour les opérations flottantes, séparé du pipeline originel. Pour bien expliquer les choses, prenons le processeur illustré ci-dessous. Un processeur a deux ''back-end'' séparés, un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par l'ALU entière, passent éventuellement dans l'unité mémoire, pour ensuite arriver à l'étape de ''writeback''. La FPU et le banc de registre flottant ont eux aussi leur propre ''back-end'' séparé.
[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]]
Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaqaue pipeline, à chaque cycle.
L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine.
Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants.
===La double émission entière===
Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait.
Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
==Les unités de calcul d'un processeur superscalaire==
Après avoir vu le chargement et le décodage superscalaire, voyons ce qu'il en est au niveau des unités de calcul. Nous allons parler des unités de calcul avant de parler de l'émission, pour une raison simple : l'émission fait l'interface entre unité de calcul et ''front-end'' et on ne peut pas parler de l'émission sans avoir vu comment la superscalarité impacte les deux. Si le ''front-end'' charge et décode un paquet de N instructions, son exécution demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est possible d'exploiter plusieurs unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu.
Les tout premiers CPU superscalaires étaient nombreux à utiliser cette technique. Ils étaient des CPU double émission, avec une ALU entière, une unité mémoire et une FPU. Nous parlerons de '''partitionnement INT-FLOAT-MEM'''. Il s'agit de l'HyperSPARC, de l'alpha 21064 et du PowerPC 603. Les contrainte d’appariement variaient grandement suivant le processeur. Les plus permissifs autorisaient toutes les combinaisons possibles entre : une opération entière, une opération flottante, et une unité mémoire. Mais ils n'étaient pas les seuls.
Le PowerPC 603 implémentait ce genre de double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions).
Il est aussi possible de faire la même chose pour les opérations entières, vu qu'elles sont prises en charge par trois circuits. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres. Il faut parfois lui ajouter le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible d'émettre une opération séparément, ce qui donne le '''partitionnement entier''' dans ce qui suit. Le partitionnement entier n'est jamais utilisé tel quel, car le gain en performance serait trop faible au regard des couts. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''. Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, 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]]
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications.
Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires modernes utilisent toutes ces stratégies===
Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autre unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents.
* Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU.
* La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants.
* La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables.
De nos jours, le partitionnement est utilisé en complément de la duplication des ALU entières, et de la duplication des autres unités. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993.
Le meilleur moyen de voir comment sont combinés ces quatre technique est de regarder ce qu'il s'est passé historiquement.
==L'unité d'émission d'un processeur superscalaire==
Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Voyons pourquoi.
===L'excès de ports d'émission===
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ou 5 ?
Une solution est de réduire la largeur du processeur. Dans l'exemple précédent, il est possible de passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La solution précédente est certes très intéressante, mais elle a des défauts. Le principal est que les ports d'émission sont sous-utilisés. Et les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
===L'intérieur de l'unité d'émission===
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
==L'évolution historique de la superscalarité==
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. La plupart des CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 sont rapidement montés à de la quadruple émission, avec quelques exceptions. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 ||
|-
! 1995
| 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000
|-
! 1996
| class="f_bleu | 620 || || || || class="f_bleu | 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
===Les processeurs historiques à double émission===
Pour commencer, étudions le cas des premiers CPU superscalaires à double émission. Ils sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Notamment, la duplication des autres unités ne sert pour ainsi dire à rien, ce qui limite les possibilités, mais rendra les explications plus simples.
Il pouvaient se classer en trois types principaux, assez stricts, avec quelques processeurs qui ne rentraient pas dans ces cases. Le premier utilise un ''partitionnement complet'' entre ALU, FPU et unité mémoire. Le second utilisait la double émission entière-flottante. Le troisième utilisait la double émission entière. Voici le tableau chronologique disant quel CPU utilisait quelle technique.
* Le partitionnement INT-FLOAT-MEM est en jaune.
* La double émission entière-flottante est en vert.
* La double émission entière est en rouge.
* Les CPU barrés utilisent la quadruple émission et seront vu dans la section suivante.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| class="f_vert | RS/6000 || || || class="f_rouge | i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| class="f_vert | RSC || class="f_vert | PA 7100 || SuperSPARC || || class="f_jaune | 21064 ||
|-
! 1993
| class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || ||
|-
! 1994
| <s>POWER 2</s> || PA 7200 || || || <s>21164</s> ||
|}
Vous voyez que certains processeur sont laissés de côté dans le tableau précédent. C'est car ils ne rentrent pas dans les catégories précédentes et faisaient autrement. Si on omet le Motorola 88110, qui utilisait une forme de double émission assez avancée, les deux CPUs qui vont nous intéresser sont le PA-RISC PA 7200 d'HP, et le SuperSPARC.
Le PA 7200, sorti en 1994, est un PA-7100 auquel on aurait rajouté une ALU entière. Précisons que ce processeur exécutait les multiplications entières dans la FPU, comme son prédécesseur. Les combinaisons autorisées étaient donc INT + INT, INT + MEM, INT + FLOAT/MUL, MEM + FLOAT/MUL. Les combinaisons autorisées par le partitionnement INT-FLOAT-MEM étaient donc possibles, en plus des combinaisons INT + INT et INT + INT + MUL. Autrement dit, il pouvait faire toute ce que permettait un CPU à double émission entière, plus les combinaisons FLOAT/MUL + MEM.
Le SuperSPARC était un CPU triple émission, avec deux ALU entières, une FPU et une unité mémoire. Il pouvait émettre, à chaque cycle, trois µops en piochant dans deux opérations entières, un accès mémoire, une opération flottante. Le SuperSPARC avait de nombreuses contraintes d’appariement. Par exemple, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Une bonne partie de ces contraintes était lié au banc de registre, qui n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions.
: La seconde ALU a été abandonnée dans l'HyperSPARC, qui est de plus repassé en double émission, ce qui est une évolution à rebours de l'évolution des autres CPUs. Le SuperSPARC était en avance sur son temps, ce quelques années, alors que l'HyperSPARC a un design de son temps.
===Les processeurs historiques à quadruple émission===
C'est la même année , en 1994, que la quadruple émission est arrivée et que les CPU ont commencé à avoir de plus en plus d'unités de calcul. Même les designs restés en double émission avaient ajouté des unités de calcul. Il y avait alors trois stratégies pour la quadruple émission.
La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Les FPUs étaient soit dupliquées, soit scindées en deux en séparant l'additionneur du multiplieur flottant. Le POWER 2 dupliquait ses FPUs à la perfection, alors que l'21264 partitionnait la FPU. Les accès mémoire étaient traités comme des opérations entières, et le processeur pouvait : soit en faire un seul par cycle, soit en émettre deux, sous conditions (le cache devait être double port pour lire/écrire deux données à la fois, comme on le verra dans la suite du chapitre).
Les processeurs MIPS R8000 et R10000 allaient un peu plus loin, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités.
La seconde implémentation dupliquait l'ALU entière et combinait le tout avec un partitionnement INT-FLOAT-MEM. De quoi exécuter en même temps deux opérations entières, une opération flottante, et un accès mémoire. 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).
Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, avec au minimum deux ALU entières. Pour comprendre ce que je veux dire, prenons l'exemple de l'UltraSPARC. Il avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplier flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes. Le processeur pouvait émettre 4 instructions, tant qu'elles atterrissaient dans des unités différentes.
La même méthode peut être utilisée pour la double émission. Le Motorola 88110, sorti en 1991, était un CPU à double émission avec pas moins de 10 unités : deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise.
La morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes. Le tout est résumé dans le tableau ci-dessous, avec :
* en vert les CPU utilisant la première stratégie ;
* en rouge les CPU utilisant la seconde stratégie ;
* en jaune les CPU utilisant la troisième stratégie.
{|class="wikitable"
|+ Processeurs quadruple émission historiques
|-
! Année !! Processeur !! Unités
|-
! 1991
| class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques
|-
|-
! rowspan="2" | 1994
| class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'')
|-
| class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM
|-
! rowspan="3" | 1995
| class="f_jaune | PA 8000 || class="f_jaune | 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD
|-
| class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques
|-
| MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM
|-
! rowspan="2" | 1996
| class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM
|-
| class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL)
|}
===Les processeurs superscalaires modernes===
Les CPU superscalaires modernes ont des design légèrement différents des CPU historiques. Ils dupliquent l'unité entière, mais leur partitionnement est différent. En général, les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Par exemple, un CPU à 6 voies pourra émettre/exécuter 6 opérations entières simultanément, grâce à 6 ALU entières. Mais il s'agit là d'un maximum et certaines peuvent être remplacées par une opération flottante ou mémoire, qui utilisent l'unité mémoire et/ou la FPU.
Un exemple assez parlant est les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon. C'était une microarchitecture à triple émission, avec trois ALU entières et trois unités mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements.
Il s'agit là d'une tendance, cependant. Les CPU superscalaires tendent à mettre le paquet niveau opérations arithmétiques, mais pas forcément au point de remplir la totalité de la largeur du processeur. La règle est que pour N voies, le processeur dispose d'au moins N/2 ALU entières. Attention, cela ne signifie pas que le processeur émet systématiquement N/2 opération entières. Il peut par exemple émettre une combinaison d'opérations flottantes et mémoire, avec des branchements. Et N/2 est une borne basse, la plupart des CPU x86 ont environ tournent plutôt autour des 2/3, voire des 3/4.
En général, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. La conséquence est que les unités mémoire sont dupliquées, en seconde place dans l'ordre d'importance. Les processeurs x86 ont commencé à dupliquer leurs unités mémoire dès qu'ils ont atteint la quadruple émission, pour les CPU grand public. La microarchitecture Core d'Intel utilisait la quadruple émission, avec deux unités mémoire et trois ALU entières. Les microarchitectures d'AMD K5 et K6 utilisaient aussi la quadruple émission et avaient deux ALUs pour deux unités mémoire.
Plus récemment, la microarchitecture ''Golden Cove'' dispose de cinq unités de calcul trois unités mémoire (deux spécialisées dans les écritures, trois dans les lectures). La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire.Il y a donc soit égalité, soit avantage pour les ALU entières.
Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Le banc de registre d'un processeur superscalaire===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Une autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions auto-aligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions auto-aligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez longs, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
c1bbt8kv2wkxsjjgeu01ovkm4fdtfuc
773165
773164
2026-09-25T19:28:06Z
Mewtow
31375
/* L'unité d'émission d'un processeur superscalaire */
773165
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===L'impact des contraintes d’appariement sur la performance===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour la suite du chapitre===
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==Le ''front-end'' des processeurs superscalaires==
Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple.
Intuitivement, charger, décoder et 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 certaines processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. 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
|}
Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs.
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===L'émission parallèle des instructions===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elle sont propagée au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage).
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
==Les processeurs implémentés avec plusieurs pipelines==
Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Vous vous dites que l'implémentation d'un processeur superscalaire n'est pas la même suivant que le CPU utilise l'exécution dans le désordre, ou non. Mais dans les faits, la distinction est plus subtile que ca. Pour l'expliquer, je vais utiliser une classification originale, qui sépare les processeurs superscalaires en trois sous-types. La distinction se fait sur la manière dont la superscalarité est implémentée en matériel.La classification que j'utilise distingue :
* Les processeurs avec plusieurs pipelines, sans exécution dans le désordre.
* Les processeurs superscalaires avec un pipeline dynamique.
* Les processeurs avec des fenêtres d'instruction décentralisées.
La distinction n'est pas qu'une question d'implémentation matérielle. Elle impacte aussi le ratio entre instructions chargées/décodées et instructions émises. Vous vous dites que les deux sont égales, mais ce n'est le cas que sur les processeurs avec N pipelines ! Sur les autres processeurs, il est possible de décoder 4 instructions, mais d'en émettre maximum 6 par cycle ! Et sur les CPU avec des fenêtres d'instruction décentralisées, il y a aussi une différence entre le nombre d'instructions qui sont ''dispatch'' et ''schedule''.
La catégorie la plus simple est celle des '''processeurs avec plusieurs pipelines'''. Ce sont des processeurs superscalaires sans exécution dans le désordre. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction.
Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, d'une FPU, éventuellement d'un second banc de registres, guère plus. Il permet d'exécuter une seconde opération de calcul, pas plus, ce qui est à l'origine de contraintes d'appariemment assez fortes. De plus, il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section.
===La double émission entière-flottante===
L'exemple le plus simple utilise un pipeline pour les opérations flottantes, séparé du pipeline originel. Pour bien expliquer les choses, prenons le processeur illustré ci-dessous. Un processeur a deux ''back-end'' séparés, un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par l'ALU entière, passent éventuellement dans l'unité mémoire, pour ensuite arriver à l'étape de ''writeback''. La FPU et le banc de registre flottant ont eux aussi leur propre ''back-end'' séparé.
[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]]
Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaqaue pipeline, à chaque cycle.
L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine.
Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants.
===La double émission entière===
Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait.
Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
==Les unités de calcul d'un processeur superscalaire==
Après avoir vu le chargement et le décodage superscalaire, voyons ce qu'il en est au niveau des unités de calcul. Nous allons parler des unités de calcul avant de parler de l'émission, pour une raison simple : l'émission fait l'interface entre unité de calcul et ''front-end'' et on ne peut pas parler de l'émission sans avoir vu comment la superscalarité impacte les deux. Si le ''front-end'' charge et décode un paquet de N instructions, son exécution demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est possible d'exploiter plusieurs unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu.
Les tout premiers CPU superscalaires étaient nombreux à utiliser cette technique. Ils étaient des CPU double émission, avec une ALU entière, une unité mémoire et une FPU. Nous parlerons de '''partitionnement INT-FLOAT-MEM'''. Il s'agit de l'HyperSPARC, de l'alpha 21064 et du PowerPC 603. Les contrainte d’appariement variaient grandement suivant le processeur. Les plus permissifs autorisaient toutes les combinaisons possibles entre : une opération entière, une opération flottante, et une unité mémoire. Mais ils n'étaient pas les seuls.
Le PowerPC 603 implémentait ce genre de double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions).
Il est aussi possible de faire la même chose pour les opérations entières, vu qu'elles sont prises en charge par trois circuits. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres. Il faut parfois lui ajouter le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible d'émettre une opération séparément, ce qui donne le '''partitionnement entier''' dans ce qui suit. Le partitionnement entier n'est jamais utilisé tel quel, car le gain en performance serait trop faible au regard des couts. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''. Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, 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]]
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications.
Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires modernes utilisent toutes ces stratégies===
Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autre unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents.
* Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU.
* La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants.
* La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables.
De nos jours, le partitionnement est utilisé en complément de la duplication des ALU entières, et de la duplication des autres unités. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993.
Le meilleur moyen de voir comment sont combinés ces quatre technique est de regarder ce qu'il s'est passé historiquement.
==L'unité d'émission d'un processeur superscalaire==
Les processeurs précédents, avec 2 pipelines, avaient une unité d'émission assez simple. Elle avait deux ports de décodage, et deux ports d'émission. Le nombre de ports de décodage et d'émission étaient identiques. Vous vous dites que c'est naturel, et intuitif, que c'est normal pour un processeur superscalaire. Mais en réalité, la majorité des processeurs superscalaires a plus de ports d'émission que de décodage. Voyons pourquoi.
===L'excès de ports d'émission===
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ou 5 ?
Une solution est de réduire la largeur du processeur. Dans l'exemple précédent, il est possible de passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La solution précédente est certes très intéressante, mais elle a des défauts. Le principal est que les ports d'émission sont sous-utilisés. Et les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
===L'intérieur de l'unité d'émission===
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
==L'évolution historique de la superscalarité==
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. La plupart des CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 sont rapidement montés à de la quadruple émission, avec quelques exceptions. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 ||
|-
! 1995
| 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000
|-
! 1996
| class="f_bleu | 620 || || || || class="f_bleu | 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
===Les processeurs historiques à double émission===
Pour commencer, étudions le cas des premiers CPU superscalaires à double émission. Ils sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Notamment, la duplication des autres unités ne sert pour ainsi dire à rien, ce qui limite les possibilités, mais rendra les explications plus simples.
Il pouvaient se classer en trois types principaux, assez stricts, avec quelques processeurs qui ne rentraient pas dans ces cases. Le premier utilise un ''partitionnement complet'' entre ALU, FPU et unité mémoire. Le second utilisait la double émission entière-flottante. Le troisième utilisait la double émission entière. Voici le tableau chronologique disant quel CPU utilisait quelle technique.
* Le partitionnement INT-FLOAT-MEM est en jaune.
* La double émission entière-flottante est en vert.
* La double émission entière est en rouge.
* Les CPU barrés utilisent la quadruple émission et seront vu dans la section suivante.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| class="f_vert | RS/6000 || || || class="f_rouge | i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| class="f_vert | RSC || class="f_vert | PA 7100 || SuperSPARC || || class="f_jaune | 21064 ||
|-
! 1993
| class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || ||
|-
! 1994
| <s>POWER 2</s> || PA 7200 || || || <s>21164</s> ||
|}
Vous voyez que certains processeur sont laissés de côté dans le tableau précédent. C'est car ils ne rentrent pas dans les catégories précédentes et faisaient autrement. Si on omet le Motorola 88110, qui utilisait une forme de double émission assez avancée, les deux CPUs qui vont nous intéresser sont le PA-RISC PA 7200 d'HP, et le SuperSPARC.
Le PA 7200, sorti en 1994, est un PA-7100 auquel on aurait rajouté une ALU entière. Précisons que ce processeur exécutait les multiplications entières dans la FPU, comme son prédécesseur. Les combinaisons autorisées étaient donc INT + INT, INT + MEM, INT + FLOAT/MUL, MEM + FLOAT/MUL. Les combinaisons autorisées par le partitionnement INT-FLOAT-MEM étaient donc possibles, en plus des combinaisons INT + INT et INT + INT + MUL. Autrement dit, il pouvait faire toute ce que permettait un CPU à double émission entière, plus les combinaisons FLOAT/MUL + MEM.
Le SuperSPARC était un CPU triple émission, avec deux ALU entières, une FPU et une unité mémoire. Il pouvait émettre, à chaque cycle, trois µops en piochant dans deux opérations entières, un accès mémoire, une opération flottante. Le SuperSPARC avait de nombreuses contraintes d’appariement. Par exemple, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Une bonne partie de ces contraintes était lié au banc de registre, qui n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions.
: La seconde ALU a été abandonnée dans l'HyperSPARC, qui est de plus repassé en double émission, ce qui est une évolution à rebours de l'évolution des autres CPUs. Le SuperSPARC était en avance sur son temps, ce quelques années, alors que l'HyperSPARC a un design de son temps.
===Les processeurs historiques à quadruple émission===
C'est la même année , en 1994, que la quadruple émission est arrivée et que les CPU ont commencé à avoir de plus en plus d'unités de calcul. Même les designs restés en double émission avaient ajouté des unités de calcul. Il y avait alors trois stratégies pour la quadruple émission.
La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Les FPUs étaient soit dupliquées, soit scindées en deux en séparant l'additionneur du multiplieur flottant. Le POWER 2 dupliquait ses FPUs à la perfection, alors que l'21264 partitionnait la FPU. Les accès mémoire étaient traités comme des opérations entières, et le processeur pouvait : soit en faire un seul par cycle, soit en émettre deux, sous conditions (le cache devait être double port pour lire/écrire deux données à la fois, comme on le verra dans la suite du chapitre).
Les processeurs MIPS R8000 et R10000 allaient un peu plus loin, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités.
La seconde implémentation dupliquait l'ALU entière et combinait le tout avec un partitionnement INT-FLOAT-MEM. De quoi exécuter en même temps deux opérations entières, une opération flottante, et un accès mémoire. 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).
Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, avec au minimum deux ALU entières. Pour comprendre ce que je veux dire, prenons l'exemple de l'UltraSPARC. Il avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplier flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes. Le processeur pouvait émettre 4 instructions, tant qu'elles atterrissaient dans des unités différentes.
La même méthode peut être utilisée pour la double émission. Le Motorola 88110, sorti en 1991, était un CPU à double émission avec pas moins de 10 unités : deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise.
La morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes. Le tout est résumé dans le tableau ci-dessous, avec :
* en vert les CPU utilisant la première stratégie ;
* en rouge les CPU utilisant la seconde stratégie ;
* en jaune les CPU utilisant la troisième stratégie.
{|class="wikitable"
|+ Processeurs quadruple émission historiques
|-
! Année !! Processeur !! Unités
|-
! 1991
| class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques
|-
|-
! rowspan="2" | 1994
| class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'')
|-
| class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM
|-
! rowspan="3" | 1995
| class="f_jaune | PA 8000 || class="f_jaune | 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD
|-
| class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques
|-
| MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM
|-
! rowspan="2" | 1996
| class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM
|-
| class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL)
|}
===Les processeurs superscalaires modernes===
Les CPU superscalaires modernes ont des design légèrement différents des CPU historiques. Ils dupliquent l'unité entière, mais leur partitionnement est différent. En général, les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Par exemple, un CPU à 6 voies pourra émettre/exécuter 6 opérations entières simultanément, grâce à 6 ALU entières. Mais il s'agit là d'un maximum et certaines peuvent être remplacées par une opération flottante ou mémoire, qui utilisent l'unité mémoire et/ou la FPU.
Un exemple assez parlant est les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon. C'était une microarchitecture à triple émission, avec trois ALU entières et trois unités mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements.
Il s'agit là d'une tendance, cependant. Les CPU superscalaires tendent à mettre le paquet niveau opérations arithmétiques, mais pas forcément au point de remplir la totalité de la largeur du processeur. La règle est que pour N voies, le processeur dispose d'au moins N/2 ALU entières. Attention, cela ne signifie pas que le processeur émet systématiquement N/2 opération entières. Il peut par exemple émettre une combinaison d'opérations flottantes et mémoire, avec des branchements. Et N/2 est une borne basse, la plupart des CPU x86 ont environ tournent plutôt autour des 2/3, voire des 3/4.
En général, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. La conséquence est que les unités mémoire sont dupliquées, en seconde place dans l'ordre d'importance. Les processeurs x86 ont commencé à dupliquer leurs unités mémoire dès qu'ils ont atteint la quadruple émission, pour les CPU grand public. La microarchitecture Core d'Intel utilisait la quadruple émission, avec deux unités mémoire et trois ALU entières. Les microarchitectures d'AMD K5 et K6 utilisaient aussi la quadruple émission et avaient deux ALUs pour deux unités mémoire.
Plus récemment, la microarchitecture ''Golden Cove'' dispose de cinq unités de calcul trois unités mémoire (deux spécialisées dans les écritures, trois dans les lectures). La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire.Il y a donc soit égalité, soit avantage pour les ALU entières.
Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Le banc de registre d'un processeur superscalaire===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Une autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions auto-aligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions auto-aligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez longs, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
jhhl9tubcqk7sj493go3szaa47yrjvm
773166
773165
2026-09-25T20:01:19Z
Mewtow
31375
773166
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===L'impact des contraintes d’appariement sur la performance===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour la suite du chapitre===
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==Le ''front-end'' des processeurs superscalaires==
Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple.
Intuitivement, charger, décoder et 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 certaines processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. 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
|}
Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs.
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===L'émission parallèle des instructions===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elle sont propagée au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage).
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
==Les processeurs implémentés avec plusieurs pipelines==
Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Vous vous dites que l'implémentation d'un processeur superscalaire n'est pas la même suivant que le CPU utilise l'exécution dans le désordre, ou non. Mais dans les faits, la distinction est plus subtile que ca. Pour l'expliquer, je vais utiliser une classification originale, qui sépare les processeurs superscalaires en trois sous-types. La distinction se fait sur la manière dont la superscalarité est implémentée en matériel.La classification que j'utilise distingue :
* Les processeurs avec plusieurs pipelines, sans exécution dans le désordre.
* Les processeurs superscalaires avec un pipeline dynamique.
* Les processeurs avec des fenêtres d'instruction décentralisées.
La distinction n'est pas qu'une question d'implémentation matérielle. Elle impacte aussi le ratio entre instructions chargées/décodées et instructions émises. Vous vous dites que les deux sont égales, mais ce n'est le cas que sur les processeurs avec N pipelines ! Sur les autres processeurs, il est possible de décoder 4 instructions, mais d'en émettre maximum 6 par cycle ! Et sur les CPU avec des fenêtres d'instruction décentralisées, il y a aussi une différence entre le nombre d'instructions qui sont ''dispatch'' et ''schedule''.
La catégorie la plus simple est celle des '''processeurs avec plusieurs pipelines'''. Ce sont des processeurs superscalaires sans exécution dans le désordre. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction.
Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, d'une FPU, éventuellement d'un second banc de registres, guère plus. Il permet d'exécuter une seconde opération de calcul, pas plus, ce qui est à l'origine de contraintes d'appariemment assez fortes. De plus, il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section.
===La double émission entière-flottante===
L'exemple le plus simple utilise un pipeline pour les opérations flottantes, séparé du pipeline originel. Pour bien expliquer les choses, prenons le processeur illustré ci-dessous. Un processeur a deux ''back-end'' séparés, un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par l'ALU entière, passent éventuellement dans l'unité mémoire, pour ensuite arriver à l'étape de ''writeback''. La FPU et le banc de registre flottant ont eux aussi leur propre ''back-end'' séparé.
[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]]
Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaqaue pipeline, à chaque cycle.
L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine.
Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants.
===La double émission entière===
Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait.
Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
==Les processeurs superscalaires dynamiques==
Les processeurs précédents, avec 2 pipelines, avaient une unité d'émission assez simple. Elle avait deux ports de décodage, et deux ports d'émission. Le nombre de ports de décodage et d'émission étaient identiques. Vous vous dites que c'est naturel, et intuitif, que c'est normal pour un processeur superscalaire. Mais en réalité, la majorité des processeurs superscalaires a plus de ports d'émission que de décodage. Expliquer pourquoi va cependant nous demander de parler de comment la superscalarité exploite les unités de calcul.
Posons-nous cette question : de combien d'unités un processeur superscalaire a besoin ? Les unités en question ne sont nécessairement des unité de calcul, l'unité mémoire et l'unité de branchement comptent aussi. La réponse dépend du nombre de voies. Si le ''front-end'' charge et décode un paquet de N instructions, exécuter ces N instructions simultanément demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section.
La réponse dépend aussi des contraintes d’appariement. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission parfaite devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement, ce qui ferait passer de 6 unités à 12. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, le cout en circuit encore plus prohibitif !
En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée, alors que la double émission parait plus adaptée.
Et c'est là que la différence entre ports d'émission et de décodage devient intéressante. L'idée est d'avoir une unité d'émission avec deux ports de décodage, et 4 port d'émission (un par unité). Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux micro-opérations tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu.
Les tout premiers CPU superscalaires étaient nombreux à utiliser cette technique. Les exemples classiques sont l'HyperSPARC, de l'alpha 21064 et du PowerPC 603. C'étaient des CPU double émission, avec une ALU entière, une unité mémoire et une FPU. Nous parlerons de '''partitionnement INT-FLOAT-MEM'''. Les contraintes d’appariement variaient grandement suivant le processeur.
Le PowerPC 603 implémentait une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions).
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple, facile à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. La raison est que le partitionnement permet d'émettre des paires, triplets, quadruplets de micro-opérations qui sont rares en pratique.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''.
Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, 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. Et enfin, il faut rajouter un port d'émission pour pouvoir émettre une micro-opération entière en plus.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle.
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
Reste à connecter les unités de calcul à l'unité d'émission (indirectement, car il y a des étages de pipeline entre les deux). Une implémentation basique utilisee un port d'émission par unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La présence d'un circuit multiplieur et d'un ''barrel shifter'' pose des problèmes pour le partitionnement. Il est possible d'avoir un port d'émission dédié au multiplieur, et un autre au ''barrel shifter'', qui sont séparés de ceux pour l'ALU entière. Il est alors possible d'émettre une opération entière, une multiplication et un décalage, simultanément. Nous parlerons de '''partitionnement entier''' pour désigner cette possibilité.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
Le partitionnement entier est cependant très rare, car le gain en performance serait trop faible au regard des couts. Quelques processeurs historiques l'utilisaient, mais ce n'est plus le cas de nos jours. La raison est que les multiplications et les décalages sont assez rares dans le code, ce qui fait que le port d'émission associés sont sous-utilisés. Et un port d'émission a un cout en circuit assez important. 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.
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul. L'exemple classique est justement lié aux circuits multiplieur et ''barrel shifters''. L'idée est que le multiplieur et le ''barrel shifter'' sont mis sur le même port d'émission qu'une ALU entière.
Prenons par exemple le processeur à double émission entière illustré ci-contre. Un port d'émission est relié à une ALU entière seule, et l'autre port d'émission alimente la seconde ALU entière et le multiplieur. La conséquence est que le processeur peut émettre deux opérations entières, dont une multiplication. Si on avait utilisé un port d'émission dédié au multiplieur, il aurait été possible d'émettre deux opérations entières et une multiplication. On perd donc une voie, mais on gagne en flexibilité et on économise des circuits.
[[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]]
En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais le partage des ports d'émission peut appliquer à n'importe quelle unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. 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.
La solution est de partager des ports d'émission, pour passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple relie une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires modernes utilisent toutes ces stratégies===
Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autre unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents.
* Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU.
* La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants.
* La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables.
* Le '''partage des ports d'émission''' réduit le cout en circuit du CPU, pour des pertes de performances mineures si bien fait.
De nos jours, le partitionnement est utilisé en complément de la duplication des ALU entières, et de la duplication des autres unités. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993.
===L'intérieur de l'unité d'émission===
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
==L'évolution historique de la superscalarité==
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. La plupart des CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 sont rapidement montés à de la quadruple émission, avec quelques exceptions. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 ||
|-
! 1995
| 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000
|-
! 1996
| class="f_bleu | 620 || || || || class="f_bleu | 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
===Les processeurs historiques à double émission===
Pour commencer, étudions le cas des premiers CPU superscalaires à double émission. Ils sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Notamment, la duplication des autres unités ne sert pour ainsi dire à rien, ce qui limite les possibilités, mais rendra les explications plus simples.
Il pouvaient se classer en trois types principaux, assez stricts, avec quelques processeurs qui ne rentraient pas dans ces cases. Le premier utilise un ''partitionnement complet'' entre ALU, FPU et unité mémoire. Le second utilisait la double émission entière-flottante. Le troisième utilisait la double émission entière. Voici le tableau chronologique disant quel CPU utilisait quelle technique.
* Le partitionnement INT-FLOAT-MEM est en jaune.
* La double émission entière-flottante est en vert.
* La double émission entière est en rouge.
* Les CPU barrés utilisent la quadruple émission et seront vu dans la section suivante.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| class="f_vert | RS/6000 || || || class="f_rouge | i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| class="f_vert | RSC || class="f_vert | PA 7100 || SuperSPARC || || class="f_jaune | 21064 ||
|-
! 1993
| class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || ||
|-
! 1994
| <s>POWER 2</s> || PA 7200 || || || <s>21164</s> ||
|}
Vous voyez que certains processeur sont laissés de côté dans le tableau précédent. C'est car ils ne rentrent pas dans les catégories précédentes et faisaient autrement. Si on omet le Motorola 88110, qui utilisait une forme de double émission assez avancée, les deux CPUs qui vont nous intéresser sont le PA-RISC PA 7200 d'HP, et le SuperSPARC.
Le PA 7200, sorti en 1994, est un PA-7100 auquel on aurait rajouté une ALU entière. Précisons que ce processeur exécutait les multiplications entières dans la FPU, comme son prédécesseur. Les combinaisons autorisées étaient donc INT + INT, INT + MEM, INT + FLOAT/MUL, MEM + FLOAT/MUL. Les combinaisons autorisées par le partitionnement INT-FLOAT-MEM étaient donc possibles, en plus des combinaisons INT + INT et INT + INT + MUL. Autrement dit, il pouvait faire toute ce que permettait un CPU à double émission entière, plus les combinaisons FLOAT/MUL + MEM.
Le SuperSPARC était un CPU triple émission, avec deux ALU entières, une FPU et une unité mémoire. Il pouvait émettre, à chaque cycle, trois µops en piochant dans deux opérations entières, un accès mémoire, une opération flottante. Le SuperSPARC avait de nombreuses contraintes d’appariement. Par exemple, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Une bonne partie de ces contraintes était lié au banc de registre, qui n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions.
: La seconde ALU a été abandonnée dans l'HyperSPARC, qui est de plus repassé en double émission, ce qui est une évolution à rebours de l'évolution des autres CPUs. Le SuperSPARC était en avance sur son temps, ce quelques années, alors que l'HyperSPARC a un design de son temps.
===Les processeurs historiques à quadruple émission===
C'est la même année , en 1994, que la quadruple émission est arrivée et que les CPU ont commencé à avoir de plus en plus d'unités de calcul. Même les designs restés en double émission avaient ajouté des unités de calcul. Il y avait alors trois stratégies pour la quadruple émission.
La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Les FPUs étaient soit dupliquées, soit scindées en deux en séparant l'additionneur du multiplieur flottant. Le POWER 2 dupliquait ses FPUs à la perfection, alors que l'21264 partitionnait la FPU. Les accès mémoire étaient traités comme des opérations entières, et le processeur pouvait : soit en faire un seul par cycle, soit en émettre deux, sous conditions (le cache devait être double port pour lire/écrire deux données à la fois, comme on le verra dans la suite du chapitre).
Les processeurs MIPS R8000 et R10000 allaient un peu plus loin, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités.
La seconde implémentation dupliquait l'ALU entière et combinait le tout avec un partitionnement INT-FLOAT-MEM. De quoi exécuter en même temps deux opérations entières, une opération flottante, et un accès mémoire. 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).
Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, avec au minimum deux ALU entières. Pour comprendre ce que je veux dire, prenons l'exemple de l'UltraSPARC. Il avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplier flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes. Le processeur pouvait émettre 4 instructions, tant qu'elles atterrissaient dans des unités différentes.
La même méthode peut être utilisée pour la double émission. Le Motorola 88110, sorti en 1991, était un CPU à double émission avec pas moins de 10 unités : deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise.
La morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes. Le tout est résumé dans le tableau ci-dessous, avec :
* en vert les CPU utilisant la première stratégie ;
* en rouge les CPU utilisant la seconde stratégie ;
* en jaune les CPU utilisant la troisième stratégie.
{|class="wikitable"
|+ Processeurs quadruple émission historiques
|-
! Année !! Processeur !! Unités
|-
! 1991
| class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques
|-
|-
! rowspan="2" | 1994
| class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'')
|-
| class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM
|-
! rowspan="3" | 1995
| class="f_jaune | PA 8000 || class="f_jaune | 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD
|-
| class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques
|-
| MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM
|-
! rowspan="2" | 1996
| class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM
|-
| class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL)
|}
===Les processeurs superscalaires modernes===
Les CPU superscalaires modernes ont des design légèrement différents des CPU historiques. Ils dupliquent l'unité entière, mais leur partitionnement est différent. En général, les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Par exemple, un CPU à 6 voies pourra émettre/exécuter 6 opérations entières simultanément, grâce à 6 ALU entières. Mais il s'agit là d'un maximum et certaines peuvent être remplacées par une opération flottante ou mémoire, qui utilisent l'unité mémoire et/ou la FPU.
Un exemple assez parlant est les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon. C'était une microarchitecture à triple émission, avec trois ALU entières et trois unités mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements.
Il s'agit là d'une tendance, cependant. Les CPU superscalaires tendent à mettre le paquet niveau opérations arithmétiques, mais pas forcément au point de remplir la totalité de la largeur du processeur. La règle est que pour N voies, le processeur dispose d'au moins N/2 ALU entières. Attention, cela ne signifie pas que le processeur émet systématiquement N/2 opération entières. Il peut par exemple émettre une combinaison d'opérations flottantes et mémoire, avec des branchements. Et N/2 est une borne basse, la plupart des CPU x86 ont environ tournent plutôt autour des 2/3, voire des 3/4.
En général, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. La conséquence est que les unités mémoire sont dupliquées, en seconde place dans l'ordre d'importance. Les processeurs x86 ont commencé à dupliquer leurs unités mémoire dès qu'ils ont atteint la quadruple émission, pour les CPU grand public. La microarchitecture Core d'Intel utilisait la quadruple émission, avec deux unités mémoire et trois ALU entières. Les microarchitectures d'AMD K5 et K6 utilisaient aussi la quadruple émission et avaient deux ALUs pour deux unités mémoire.
Plus récemment, la microarchitecture ''Golden Cove'' dispose de cinq unités de calcul trois unités mémoire (deux spécialisées dans les écritures, trois dans les lectures). La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire.Il y a donc soit égalité, soit avantage pour les ALU entières.
Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Le banc de registre d'un processeur superscalaire===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Une autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions auto-aligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions auto-aligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez longs, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
cic8zonoxmzz8xdicx3vffooo4xe8ad
773167
773166
2026-09-25T20:01:43Z
Mewtow
31375
/* L'intérieur de l'unité d'émission */
773167
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===L'impact des contraintes d’appariement sur la performance===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour la suite du chapitre===
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==Le ''front-end'' des processeurs superscalaires==
Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple.
Intuitivement, charger, décoder et 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 certaines processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. 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
|}
Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs.
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===L'émission parallèle des instructions===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elle sont propagée au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage).
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
==Les processeurs implémentés avec plusieurs pipelines==
Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Vous vous dites que l'implémentation d'un processeur superscalaire n'est pas la même suivant que le CPU utilise l'exécution dans le désordre, ou non. Mais dans les faits, la distinction est plus subtile que ca. Pour l'expliquer, je vais utiliser une classification originale, qui sépare les processeurs superscalaires en trois sous-types. La distinction se fait sur la manière dont la superscalarité est implémentée en matériel.La classification que j'utilise distingue :
* Les processeurs avec plusieurs pipelines, sans exécution dans le désordre.
* Les processeurs superscalaires avec un pipeline dynamique.
* Les processeurs avec des fenêtres d'instruction décentralisées.
La distinction n'est pas qu'une question d'implémentation matérielle. Elle impacte aussi le ratio entre instructions chargées/décodées et instructions émises. Vous vous dites que les deux sont égales, mais ce n'est le cas que sur les processeurs avec N pipelines ! Sur les autres processeurs, il est possible de décoder 4 instructions, mais d'en émettre maximum 6 par cycle ! Et sur les CPU avec des fenêtres d'instruction décentralisées, il y a aussi une différence entre le nombre d'instructions qui sont ''dispatch'' et ''schedule''.
La catégorie la plus simple est celle des '''processeurs avec plusieurs pipelines'''. Ce sont des processeurs superscalaires sans exécution dans le désordre. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction.
Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, d'une FPU, éventuellement d'un second banc de registres, guère plus. Il permet d'exécuter une seconde opération de calcul, pas plus, ce qui est à l'origine de contraintes d'appariemment assez fortes. De plus, il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section.
===La double émission entière-flottante===
L'exemple le plus simple utilise un pipeline pour les opérations flottantes, séparé du pipeline originel. Pour bien expliquer les choses, prenons le processeur illustré ci-dessous. Un processeur a deux ''back-end'' séparés, un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par l'ALU entière, passent éventuellement dans l'unité mémoire, pour ensuite arriver à l'étape de ''writeback''. La FPU et le banc de registre flottant ont eux aussi leur propre ''back-end'' séparé.
[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]]
Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaqaue pipeline, à chaque cycle.
L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine.
Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants.
===La double émission entière===
Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait.
Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
==Les processeurs superscalaires dynamiques==
Les processeurs précédents, avec 2 pipelines, avaient une unité d'émission assez simple. Elle avait deux ports de décodage, et deux ports d'émission. Le nombre de ports de décodage et d'émission étaient identiques. Vous vous dites que c'est naturel, et intuitif, que c'est normal pour un processeur superscalaire. Mais en réalité, la majorité des processeurs superscalaires a plus de ports d'émission que de décodage. Expliquer pourquoi va cependant nous demander de parler de comment la superscalarité exploite les unités de calcul.
Posons-nous cette question : de combien d'unités un processeur superscalaire a besoin ? Les unités en question ne sont nécessairement des unité de calcul, l'unité mémoire et l'unité de branchement comptent aussi. La réponse dépend du nombre de voies. Si le ''front-end'' charge et décode un paquet de N instructions, exécuter ces N instructions simultanément demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section.
La réponse dépend aussi des contraintes d’appariement. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission parfaite devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement, ce qui ferait passer de 6 unités à 12. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, le cout en circuit encore plus prohibitif !
En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée, alors que la double émission parait plus adaptée.
Et c'est là que la différence entre ports d'émission et de décodage devient intéressante. L'idée est d'avoir une unité d'émission avec deux ports de décodage, et 4 port d'émission (un par unité). Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux micro-opérations tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu.
Les tout premiers CPU superscalaires étaient nombreux à utiliser cette technique. Les exemples classiques sont l'HyperSPARC, de l'alpha 21064 et du PowerPC 603. C'étaient des CPU double émission, avec une ALU entière, une unité mémoire et une FPU. Nous parlerons de '''partitionnement INT-FLOAT-MEM'''. Les contraintes d’appariement variaient grandement suivant le processeur.
Le PowerPC 603 implémentait une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions).
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple, facile à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. La raison est que le partitionnement permet d'émettre des paires, triplets, quadruplets de micro-opérations qui sont rares en pratique.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''.
Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, 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. Et enfin, il faut rajouter un port d'émission pour pouvoir émettre une micro-opération entière en plus.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle.
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
Reste à connecter les unités de calcul à l'unité d'émission (indirectement, car il y a des étages de pipeline entre les deux). Une implémentation basique utilisee un port d'émission par unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La présence d'un circuit multiplieur et d'un ''barrel shifter'' pose des problèmes pour le partitionnement. Il est possible d'avoir un port d'émission dédié au multiplieur, et un autre au ''barrel shifter'', qui sont séparés de ceux pour l'ALU entière. Il est alors possible d'émettre une opération entière, une multiplication et un décalage, simultanément. Nous parlerons de '''partitionnement entier''' pour désigner cette possibilité.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
Le partitionnement entier est cependant très rare, car le gain en performance serait trop faible au regard des couts. Quelques processeurs historiques l'utilisaient, mais ce n'est plus le cas de nos jours. La raison est que les multiplications et les décalages sont assez rares dans le code, ce qui fait que le port d'émission associés sont sous-utilisés. Et un port d'émission a un cout en circuit assez important. 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.
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul. L'exemple classique est justement lié aux circuits multiplieur et ''barrel shifters''. L'idée est que le multiplieur et le ''barrel shifter'' sont mis sur le même port d'émission qu'une ALU entière.
Prenons par exemple le processeur à double émission entière illustré ci-contre. Un port d'émission est relié à une ALU entière seule, et l'autre port d'émission alimente la seconde ALU entière et le multiplieur. La conséquence est que le processeur peut émettre deux opérations entières, dont une multiplication. Si on avait utilisé un port d'émission dédié au multiplieur, il aurait été possible d'émettre deux opérations entières et une multiplication. On perd donc une voie, mais on gagne en flexibilité et on économise des circuits.
[[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]]
En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais le partage des ports d'émission peut appliquer à n'importe quelle unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. 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.
La solution est de partager des ports d'émission, pour passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple relie une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires modernes utilisent toutes ces stratégies===
Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autre unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents.
* Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU.
* La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants.
* La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables.
* Le '''partage des ports d'émission''' réduit le cout en circuit du CPU, pour des pertes de performances mineures si bien fait.
De nos jours, le partitionnement est utilisé en complément de la duplication des ALU entières, et de la duplication des autres unités. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993.
==L'évolution historique de la superscalarité==
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. La plupart des CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 sont rapidement montés à de la quadruple émission, avec quelques exceptions. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 ||
|-
! 1995
| 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000
|-
! 1996
| class="f_bleu | 620 || || || || class="f_bleu | 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
===Les processeurs historiques à double émission===
Pour commencer, étudions le cas des premiers CPU superscalaires à double émission. Ils sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Notamment, la duplication des autres unités ne sert pour ainsi dire à rien, ce qui limite les possibilités, mais rendra les explications plus simples.
Il pouvaient se classer en trois types principaux, assez stricts, avec quelques processeurs qui ne rentraient pas dans ces cases. Le premier utilise un ''partitionnement complet'' entre ALU, FPU et unité mémoire. Le second utilisait la double émission entière-flottante. Le troisième utilisait la double émission entière. Voici le tableau chronologique disant quel CPU utilisait quelle technique.
* Le partitionnement INT-FLOAT-MEM est en jaune.
* La double émission entière-flottante est en vert.
* La double émission entière est en rouge.
* Les CPU barrés utilisent la quadruple émission et seront vu dans la section suivante.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| class="f_vert | RS/6000 || || || class="f_rouge | i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| class="f_vert | RSC || class="f_vert | PA 7100 || SuperSPARC || || class="f_jaune | 21064 ||
|-
! 1993
| class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || ||
|-
! 1994
| <s>POWER 2</s> || PA 7200 || || || <s>21164</s> ||
|}
Vous voyez que certains processeur sont laissés de côté dans le tableau précédent. C'est car ils ne rentrent pas dans les catégories précédentes et faisaient autrement. Si on omet le Motorola 88110, qui utilisait une forme de double émission assez avancée, les deux CPUs qui vont nous intéresser sont le PA-RISC PA 7200 d'HP, et le SuperSPARC.
Le PA 7200, sorti en 1994, est un PA-7100 auquel on aurait rajouté une ALU entière. Précisons que ce processeur exécutait les multiplications entières dans la FPU, comme son prédécesseur. Les combinaisons autorisées étaient donc INT + INT, INT + MEM, INT + FLOAT/MUL, MEM + FLOAT/MUL. Les combinaisons autorisées par le partitionnement INT-FLOAT-MEM étaient donc possibles, en plus des combinaisons INT + INT et INT + INT + MUL. Autrement dit, il pouvait faire toute ce que permettait un CPU à double émission entière, plus les combinaisons FLOAT/MUL + MEM.
Le SuperSPARC était un CPU triple émission, avec deux ALU entières, une FPU et une unité mémoire. Il pouvait émettre, à chaque cycle, trois µops en piochant dans deux opérations entières, un accès mémoire, une opération flottante. Le SuperSPARC avait de nombreuses contraintes d’appariement. Par exemple, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Une bonne partie de ces contraintes était lié au banc de registre, qui n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions.
: La seconde ALU a été abandonnée dans l'HyperSPARC, qui est de plus repassé en double émission, ce qui est une évolution à rebours de l'évolution des autres CPUs. Le SuperSPARC était en avance sur son temps, ce quelques années, alors que l'HyperSPARC a un design de son temps.
===Les processeurs historiques à quadruple émission===
C'est la même année , en 1994, que la quadruple émission est arrivée et que les CPU ont commencé à avoir de plus en plus d'unités de calcul. Même les designs restés en double émission avaient ajouté des unités de calcul. Il y avait alors trois stratégies pour la quadruple émission.
La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Les FPUs étaient soit dupliquées, soit scindées en deux en séparant l'additionneur du multiplieur flottant. Le POWER 2 dupliquait ses FPUs à la perfection, alors que l'21264 partitionnait la FPU. Les accès mémoire étaient traités comme des opérations entières, et le processeur pouvait : soit en faire un seul par cycle, soit en émettre deux, sous conditions (le cache devait être double port pour lire/écrire deux données à la fois, comme on le verra dans la suite du chapitre).
Les processeurs MIPS R8000 et R10000 allaient un peu plus loin, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités.
La seconde implémentation dupliquait l'ALU entière et combinait le tout avec un partitionnement INT-FLOAT-MEM. De quoi exécuter en même temps deux opérations entières, une opération flottante, et un accès mémoire. 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).
Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, avec au minimum deux ALU entières. Pour comprendre ce que je veux dire, prenons l'exemple de l'UltraSPARC. Il avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplier flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes. Le processeur pouvait émettre 4 instructions, tant qu'elles atterrissaient dans des unités différentes.
La même méthode peut être utilisée pour la double émission. Le Motorola 88110, sorti en 1991, était un CPU à double émission avec pas moins de 10 unités : deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise.
La morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes. Le tout est résumé dans le tableau ci-dessous, avec :
* en vert les CPU utilisant la première stratégie ;
* en rouge les CPU utilisant la seconde stratégie ;
* en jaune les CPU utilisant la troisième stratégie.
{|class="wikitable"
|+ Processeurs quadruple émission historiques
|-
! Année !! Processeur !! Unités
|-
! 1991
| class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques
|-
|-
! rowspan="2" | 1994
| class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'')
|-
| class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM
|-
! rowspan="3" | 1995
| class="f_jaune | PA 8000 || class="f_jaune | 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD
|-
| class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques
|-
| MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM
|-
! rowspan="2" | 1996
| class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM
|-
| class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL)
|}
===Les processeurs superscalaires modernes===
Les CPU superscalaires modernes ont des design légèrement différents des CPU historiques. Ils dupliquent l'unité entière, mais leur partitionnement est différent. En général, les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Par exemple, un CPU à 6 voies pourra émettre/exécuter 6 opérations entières simultanément, grâce à 6 ALU entières. Mais il s'agit là d'un maximum et certaines peuvent être remplacées par une opération flottante ou mémoire, qui utilisent l'unité mémoire et/ou la FPU.
Un exemple assez parlant est les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon. C'était une microarchitecture à triple émission, avec trois ALU entières et trois unités mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements.
Il s'agit là d'une tendance, cependant. Les CPU superscalaires tendent à mettre le paquet niveau opérations arithmétiques, mais pas forcément au point de remplir la totalité de la largeur du processeur. La règle est que pour N voies, le processeur dispose d'au moins N/2 ALU entières. Attention, cela ne signifie pas que le processeur émet systématiquement N/2 opération entières. Il peut par exemple émettre une combinaison d'opérations flottantes et mémoire, avec des branchements. Et N/2 est une borne basse, la plupart des CPU x86 ont environ tournent plutôt autour des 2/3, voire des 3/4.
En général, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. La conséquence est que les unités mémoire sont dupliquées, en seconde place dans l'ordre d'importance. Les processeurs x86 ont commencé à dupliquer leurs unités mémoire dès qu'ils ont atteint la quadruple émission, pour les CPU grand public. La microarchitecture Core d'Intel utilisait la quadruple émission, avec deux unités mémoire et trois ALU entières. Les microarchitectures d'AMD K5 et K6 utilisaient aussi la quadruple émission et avaient deux ALUs pour deux unités mémoire.
Plus récemment, la microarchitecture ''Golden Cove'' dispose de cinq unités de calcul trois unités mémoire (deux spécialisées dans les écritures, trois dans les lectures). La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire.Il y a donc soit égalité, soit avantage pour les ALU entières.
Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Le banc de registre d'un processeur superscalaire===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Une autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions auto-aligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions auto-aligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez longs, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
aonnyqffnyj622jl8dg8sptpaig6nvz
773168
773167
2026-09-25T20:02:01Z
Mewtow
31375
/* Le banc de registre d'un processeur superscalaire */
773168
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===L'impact des contraintes d’appariement sur la performance===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour la suite du chapitre===
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==Le ''front-end'' des processeurs superscalaires==
Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple.
Intuitivement, charger, décoder et 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 certaines processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. 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
|}
Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs.
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===L'émission parallèle des instructions===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elle sont propagée au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage).
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
==Les processeurs implémentés avec plusieurs pipelines==
Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Vous vous dites que l'implémentation d'un processeur superscalaire n'est pas la même suivant que le CPU utilise l'exécution dans le désordre, ou non. Mais dans les faits, la distinction est plus subtile que ca. Pour l'expliquer, je vais utiliser une classification originale, qui sépare les processeurs superscalaires en trois sous-types. La distinction se fait sur la manière dont la superscalarité est implémentée en matériel.La classification que j'utilise distingue :
* Les processeurs avec plusieurs pipelines, sans exécution dans le désordre.
* Les processeurs superscalaires avec un pipeline dynamique.
* Les processeurs avec des fenêtres d'instruction décentralisées.
La distinction n'est pas qu'une question d'implémentation matérielle. Elle impacte aussi le ratio entre instructions chargées/décodées et instructions émises. Vous vous dites que les deux sont égales, mais ce n'est le cas que sur les processeurs avec N pipelines ! Sur les autres processeurs, il est possible de décoder 4 instructions, mais d'en émettre maximum 6 par cycle ! Et sur les CPU avec des fenêtres d'instruction décentralisées, il y a aussi une différence entre le nombre d'instructions qui sont ''dispatch'' et ''schedule''.
La catégorie la plus simple est celle des '''processeurs avec plusieurs pipelines'''. Ce sont des processeurs superscalaires sans exécution dans le désordre. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction.
Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, d'une FPU, éventuellement d'un second banc de registres, guère plus. Il permet d'exécuter une seconde opération de calcul, pas plus, ce qui est à l'origine de contraintes d'appariemment assez fortes. De plus, il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section.
===La double émission entière-flottante===
L'exemple le plus simple utilise un pipeline pour les opérations flottantes, séparé du pipeline originel. Pour bien expliquer les choses, prenons le processeur illustré ci-dessous. Un processeur a deux ''back-end'' séparés, un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par l'ALU entière, passent éventuellement dans l'unité mémoire, pour ensuite arriver à l'étape de ''writeback''. La FPU et le banc de registre flottant ont eux aussi leur propre ''back-end'' séparé.
[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]]
Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaqaue pipeline, à chaque cycle.
L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine.
Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants.
===La double émission entière===
Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait.
Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
==Les processeurs superscalaires dynamiques==
Les processeurs précédents, avec 2 pipelines, avaient une unité d'émission assez simple. Elle avait deux ports de décodage, et deux ports d'émission. Le nombre de ports de décodage et d'émission étaient identiques. Vous vous dites que c'est naturel, et intuitif, que c'est normal pour un processeur superscalaire. Mais en réalité, la majorité des processeurs superscalaires a plus de ports d'émission que de décodage. Expliquer pourquoi va cependant nous demander de parler de comment la superscalarité exploite les unités de calcul.
Posons-nous cette question : de combien d'unités un processeur superscalaire a besoin ? Les unités en question ne sont nécessairement des unité de calcul, l'unité mémoire et l'unité de branchement comptent aussi. La réponse dépend du nombre de voies. Si le ''front-end'' charge et décode un paquet de N instructions, exécuter ces N instructions simultanément demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section.
La réponse dépend aussi des contraintes d’appariement. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission parfaite devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement, ce qui ferait passer de 6 unités à 12. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, le cout en circuit encore plus prohibitif !
En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée, alors que la double émission parait plus adaptée.
Et c'est là que la différence entre ports d'émission et de décodage devient intéressante. L'idée est d'avoir une unité d'émission avec deux ports de décodage, et 4 port d'émission (un par unité). Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux micro-opérations tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu.
Les tout premiers CPU superscalaires étaient nombreux à utiliser cette technique. Les exemples classiques sont l'HyperSPARC, de l'alpha 21064 et du PowerPC 603. C'étaient des CPU double émission, avec une ALU entière, une unité mémoire et une FPU. Nous parlerons de '''partitionnement INT-FLOAT-MEM'''. Les contraintes d’appariement variaient grandement suivant le processeur.
Le PowerPC 603 implémentait une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions).
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple, facile à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. La raison est que le partitionnement permet d'émettre des paires, triplets, quadruplets de micro-opérations qui sont rares en pratique.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''.
Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, 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. Et enfin, il faut rajouter un port d'émission pour pouvoir émettre une micro-opération entière en plus.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle.
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
Reste à connecter les unités de calcul à l'unité d'émission (indirectement, car il y a des étages de pipeline entre les deux). Une implémentation basique utilisee un port d'émission par unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La présence d'un circuit multiplieur et d'un ''barrel shifter'' pose des problèmes pour le partitionnement. Il est possible d'avoir un port d'émission dédié au multiplieur, et un autre au ''barrel shifter'', qui sont séparés de ceux pour l'ALU entière. Il est alors possible d'émettre une opération entière, une multiplication et un décalage, simultanément. Nous parlerons de '''partitionnement entier''' pour désigner cette possibilité.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
Le partitionnement entier est cependant très rare, car le gain en performance serait trop faible au regard des couts. Quelques processeurs historiques l'utilisaient, mais ce n'est plus le cas de nos jours. La raison est que les multiplications et les décalages sont assez rares dans le code, ce qui fait que le port d'émission associés sont sous-utilisés. Et un port d'émission a un cout en circuit assez important. 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.
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul. L'exemple classique est justement lié aux circuits multiplieur et ''barrel shifters''. L'idée est que le multiplieur et le ''barrel shifter'' sont mis sur le même port d'émission qu'une ALU entière.
Prenons par exemple le processeur à double émission entière illustré ci-contre. Un port d'émission est relié à une ALU entière seule, et l'autre port d'émission alimente la seconde ALU entière et le multiplieur. La conséquence est que le processeur peut émettre deux opérations entières, dont une multiplication. Si on avait utilisé un port d'émission dédié au multiplieur, il aurait été possible d'émettre deux opérations entières et une multiplication. On perd donc une voie, mais on gagne en flexibilité et on économise des circuits.
[[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]]
En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais le partage des ports d'émission peut appliquer à n'importe quelle unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. 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.
La solution est de partager des ports d'émission, pour passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple relie une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires modernes utilisent toutes ces stratégies===
Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autre unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents.
* Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU.
* La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants.
* La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables.
* Le '''partage des ports d'émission''' réduit le cout en circuit du CPU, pour des pertes de performances mineures si bien fait.
De nos jours, le partitionnement est utilisé en complément de la duplication des ALU entières, et de la duplication des autres unités. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993.
==L'évolution historique de la superscalarité==
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. La plupart des CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 sont rapidement montés à de la quadruple émission, avec quelques exceptions. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 ||
|-
! 1995
| 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000
|-
! 1996
| class="f_bleu | 620 || || || || class="f_bleu | 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
===Les processeurs historiques à double émission===
Pour commencer, étudions le cas des premiers CPU superscalaires à double émission. Ils sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Notamment, la duplication des autres unités ne sert pour ainsi dire à rien, ce qui limite les possibilités, mais rendra les explications plus simples.
Il pouvaient se classer en trois types principaux, assez stricts, avec quelques processeurs qui ne rentraient pas dans ces cases. Le premier utilise un ''partitionnement complet'' entre ALU, FPU et unité mémoire. Le second utilisait la double émission entière-flottante. Le troisième utilisait la double émission entière. Voici le tableau chronologique disant quel CPU utilisait quelle technique.
* Le partitionnement INT-FLOAT-MEM est en jaune.
* La double émission entière-flottante est en vert.
* La double émission entière est en rouge.
* Les CPU barrés utilisent la quadruple émission et seront vu dans la section suivante.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| class="f_vert | RS/6000 || || || class="f_rouge | i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| class="f_vert | RSC || class="f_vert | PA 7100 || SuperSPARC || || class="f_jaune | 21064 ||
|-
! 1993
| class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || ||
|-
! 1994
| <s>POWER 2</s> || PA 7200 || || || <s>21164</s> ||
|}
Vous voyez que certains processeur sont laissés de côté dans le tableau précédent. C'est car ils ne rentrent pas dans les catégories précédentes et faisaient autrement. Si on omet le Motorola 88110, qui utilisait une forme de double émission assez avancée, les deux CPUs qui vont nous intéresser sont le PA-RISC PA 7200 d'HP, et le SuperSPARC.
Le PA 7200, sorti en 1994, est un PA-7100 auquel on aurait rajouté une ALU entière. Précisons que ce processeur exécutait les multiplications entières dans la FPU, comme son prédécesseur. Les combinaisons autorisées étaient donc INT + INT, INT + MEM, INT + FLOAT/MUL, MEM + FLOAT/MUL. Les combinaisons autorisées par le partitionnement INT-FLOAT-MEM étaient donc possibles, en plus des combinaisons INT + INT et INT + INT + MUL. Autrement dit, il pouvait faire toute ce que permettait un CPU à double émission entière, plus les combinaisons FLOAT/MUL + MEM.
Le SuperSPARC était un CPU triple émission, avec deux ALU entières, une FPU et une unité mémoire. Il pouvait émettre, à chaque cycle, trois µops en piochant dans deux opérations entières, un accès mémoire, une opération flottante. Le SuperSPARC avait de nombreuses contraintes d’appariement. Par exemple, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Une bonne partie de ces contraintes était lié au banc de registre, qui n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions.
: La seconde ALU a été abandonnée dans l'HyperSPARC, qui est de plus repassé en double émission, ce qui est une évolution à rebours de l'évolution des autres CPUs. Le SuperSPARC était en avance sur son temps, ce quelques années, alors que l'HyperSPARC a un design de son temps.
===Les processeurs historiques à quadruple émission===
C'est la même année , en 1994, que la quadruple émission est arrivée et que les CPU ont commencé à avoir de plus en plus d'unités de calcul. Même les designs restés en double émission avaient ajouté des unités de calcul. Il y avait alors trois stratégies pour la quadruple émission.
La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Les FPUs étaient soit dupliquées, soit scindées en deux en séparant l'additionneur du multiplieur flottant. Le POWER 2 dupliquait ses FPUs à la perfection, alors que l'21264 partitionnait la FPU. Les accès mémoire étaient traités comme des opérations entières, et le processeur pouvait : soit en faire un seul par cycle, soit en émettre deux, sous conditions (le cache devait être double port pour lire/écrire deux données à la fois, comme on le verra dans la suite du chapitre).
Les processeurs MIPS R8000 et R10000 allaient un peu plus loin, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités.
La seconde implémentation dupliquait l'ALU entière et combinait le tout avec un partitionnement INT-FLOAT-MEM. De quoi exécuter en même temps deux opérations entières, une opération flottante, et un accès mémoire. 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).
Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, avec au minimum deux ALU entières. Pour comprendre ce que je veux dire, prenons l'exemple de l'UltraSPARC. Il avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplier flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes. Le processeur pouvait émettre 4 instructions, tant qu'elles atterrissaient dans des unités différentes.
La même méthode peut être utilisée pour la double émission. Le Motorola 88110, sorti en 1991, était un CPU à double émission avec pas moins de 10 unités : deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise.
La morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes. Le tout est résumé dans le tableau ci-dessous, avec :
* en vert les CPU utilisant la première stratégie ;
* en rouge les CPU utilisant la seconde stratégie ;
* en jaune les CPU utilisant la troisième stratégie.
{|class="wikitable"
|+ Processeurs quadruple émission historiques
|-
! Année !! Processeur !! Unités
|-
! 1991
| class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques
|-
|-
! rowspan="2" | 1994
| class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'')
|-
| class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM
|-
! rowspan="3" | 1995
| class="f_jaune | PA 8000 || class="f_jaune | 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD
|-
| class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques
|-
| MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM
|-
! rowspan="2" | 1996
| class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM
|-
| class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL)
|}
===Les processeurs superscalaires modernes===
Les CPU superscalaires modernes ont des design légèrement différents des CPU historiques. Ils dupliquent l'unité entière, mais leur partitionnement est différent. En général, les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Par exemple, un CPU à 6 voies pourra émettre/exécuter 6 opérations entières simultanément, grâce à 6 ALU entières. Mais il s'agit là d'un maximum et certaines peuvent être remplacées par une opération flottante ou mémoire, qui utilisent l'unité mémoire et/ou la FPU.
Un exemple assez parlant est les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon. C'était une microarchitecture à triple émission, avec trois ALU entières et trois unités mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements.
Il s'agit là d'une tendance, cependant. Les CPU superscalaires tendent à mettre le paquet niveau opérations arithmétiques, mais pas forcément au point de remplir la totalité de la largeur du processeur. La règle est que pour N voies, le processeur dispose d'au moins N/2 ALU entières. Attention, cela ne signifie pas que le processeur émet systématiquement N/2 opération entières. Il peut par exemple émettre une combinaison d'opérations flottantes et mémoire, avec des branchements. Et N/2 est une borne basse, la plupart des CPU x86 ont environ tournent plutôt autour des 2/3, voire des 3/4.
En général, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. La conséquence est que les unités mémoire sont dupliquées, en seconde place dans l'ordre d'importance. Les processeurs x86 ont commencé à dupliquer leurs unités mémoire dès qu'ils ont atteint la quadruple émission, pour les CPU grand public. La microarchitecture Core d'Intel utilisait la quadruple émission, avec deux unités mémoire et trois ALU entières. Les microarchitectures d'AMD K5 et K6 utilisaient aussi la quadruple émission et avaient deux ALUs pour deux unités mémoire.
Plus récemment, la microarchitecture ''Golden Cove'' dispose de cinq unités de calcul trois unités mémoire (deux spécialisées dans les écritures, trois dans les lectures). La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire.Il y a donc soit égalité, soit avantage pour les ALU entières.
Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Le banc de registre d'un processeur superscalaire===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Une autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage.
===Les unités d'exécution dans le désordre===
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions auto-aligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions auto-aligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez longs, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
omq9vvenm5sfxyxc6sg39ns40q74ghl
773169
773168
2026-09-25T20:02:52Z
Mewtow
31375
/* Les CPU superscalaires modernes utilisent toutes ces stratégies */
773169
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===L'impact des contraintes d’appariement sur la performance===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour la suite du chapitre===
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==Le ''front-end'' des processeurs superscalaires==
Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple.
Intuitivement, charger, décoder et 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 certaines processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. 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
|}
Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs.
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===L'émission parallèle des instructions===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elle sont propagée au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage).
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
==Les processeurs implémentés avec plusieurs pipelines==
Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Vous vous dites que l'implémentation d'un processeur superscalaire n'est pas la même suivant que le CPU utilise l'exécution dans le désordre, ou non. Mais dans les faits, la distinction est plus subtile que ca. Pour l'expliquer, je vais utiliser une classification originale, qui sépare les processeurs superscalaires en trois sous-types. La distinction se fait sur la manière dont la superscalarité est implémentée en matériel.La classification que j'utilise distingue :
* Les processeurs avec plusieurs pipelines, sans exécution dans le désordre.
* Les processeurs superscalaires avec un pipeline dynamique.
* Les processeurs avec des fenêtres d'instruction décentralisées.
La distinction n'est pas qu'une question d'implémentation matérielle. Elle impacte aussi le ratio entre instructions chargées/décodées et instructions émises. Vous vous dites que les deux sont égales, mais ce n'est le cas que sur les processeurs avec N pipelines ! Sur les autres processeurs, il est possible de décoder 4 instructions, mais d'en émettre maximum 6 par cycle ! Et sur les CPU avec des fenêtres d'instruction décentralisées, il y a aussi une différence entre le nombre d'instructions qui sont ''dispatch'' et ''schedule''.
La catégorie la plus simple est celle des '''processeurs avec plusieurs pipelines'''. Ce sont des processeurs superscalaires sans exécution dans le désordre. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction.
Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, d'une FPU, éventuellement d'un second banc de registres, guère plus. Il permet d'exécuter une seconde opération de calcul, pas plus, ce qui est à l'origine de contraintes d'appariemment assez fortes. De plus, il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section.
===La double émission entière-flottante===
L'exemple le plus simple utilise un pipeline pour les opérations flottantes, séparé du pipeline originel. Pour bien expliquer les choses, prenons le processeur illustré ci-dessous. Un processeur a deux ''back-end'' séparés, un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par l'ALU entière, passent éventuellement dans l'unité mémoire, pour ensuite arriver à l'étape de ''writeback''. La FPU et le banc de registre flottant ont eux aussi leur propre ''back-end'' séparé.
[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]]
Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaqaue pipeline, à chaque cycle.
L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine.
Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants.
===La double émission entière===
Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait.
Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
==Les processeurs superscalaires dynamiques==
Les processeurs précédents, avec 2 pipelines, avaient une unité d'émission assez simple. Elle avait deux ports de décodage, et deux ports d'émission. Le nombre de ports de décodage et d'émission étaient identiques. Vous vous dites que c'est naturel, et intuitif, que c'est normal pour un processeur superscalaire. Mais en réalité, la majorité des processeurs superscalaires a plus de ports d'émission que de décodage. Expliquer pourquoi va cependant nous demander de parler de comment la superscalarité exploite les unités de calcul.
Posons-nous cette question : de combien d'unités un processeur superscalaire a besoin ? Les unités en question ne sont nécessairement des unité de calcul, l'unité mémoire et l'unité de branchement comptent aussi. La réponse dépend du nombre de voies. Si le ''front-end'' charge et décode un paquet de N instructions, exécuter ces N instructions simultanément demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section.
La réponse dépend aussi des contraintes d’appariement. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission parfaite devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement, ce qui ferait passer de 6 unités à 12. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, le cout en circuit encore plus prohibitif !
En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée, alors que la double émission parait plus adaptée.
Et c'est là que la différence entre ports d'émission et de décodage devient intéressante. L'idée est d'avoir une unité d'émission avec deux ports de décodage, et 4 port d'émission (un par unité). Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux micro-opérations tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu.
Les tout premiers CPU superscalaires étaient nombreux à utiliser cette technique. Les exemples classiques sont l'HyperSPARC, de l'alpha 21064 et du PowerPC 603. C'étaient des CPU double émission, avec une ALU entière, une unité mémoire et une FPU. Nous parlerons de '''partitionnement INT-FLOAT-MEM'''. Les contraintes d’appariement variaient grandement suivant le processeur.
Le PowerPC 603 implémentait une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions).
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple, facile à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. La raison est que le partitionnement permet d'émettre des paires, triplets, quadruplets de micro-opérations qui sont rares en pratique.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''.
Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, 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. Et enfin, il faut rajouter un port d'émission pour pouvoir émettre une micro-opération entière en plus.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle.
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
Reste à connecter les unités de calcul à l'unité d'émission (indirectement, car il y a des étages de pipeline entre les deux). Une implémentation basique utilisee un port d'émission par unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La présence d'un circuit multiplieur et d'un ''barrel shifter'' pose des problèmes pour le partitionnement. Il est possible d'avoir un port d'émission dédié au multiplieur, et un autre au ''barrel shifter'', qui sont séparés de ceux pour l'ALU entière. Il est alors possible d'émettre une opération entière, une multiplication et un décalage, simultanément. Nous parlerons de '''partitionnement entier''' pour désigner cette possibilité.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
Le partitionnement entier est cependant très rare, car le gain en performance serait trop faible au regard des couts. Quelques processeurs historiques l'utilisaient, mais ce n'est plus le cas de nos jours. La raison est que les multiplications et les décalages sont assez rares dans le code, ce qui fait que le port d'émission associés sont sous-utilisés. Et un port d'émission a un cout en circuit assez important. 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.
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul. L'exemple classique est justement lié aux circuits multiplieur et ''barrel shifters''. L'idée est que le multiplieur et le ''barrel shifter'' sont mis sur le même port d'émission qu'une ALU entière.
Prenons par exemple le processeur à double émission entière illustré ci-contre. Un port d'émission est relié à une ALU entière seule, et l'autre port d'émission alimente la seconde ALU entière et le multiplieur. La conséquence est que le processeur peut émettre deux opérations entières, dont une multiplication. Si on avait utilisé un port d'émission dédié au multiplieur, il aurait été possible d'émettre deux opérations entières et une multiplication. On perd donc une voie, mais on gagne en flexibilité et on économise des circuits.
[[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]]
En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais le partage des ports d'émission peut appliquer à n'importe quelle unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. 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.
La solution est de partager des ports d'émission, pour passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple relie une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires modernes utilisent toutes ces stratégies===
Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autre unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents.
* Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU.
* La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants.
* La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables.
* Le '''partage des ports d'émission''' réduit le cout en circuit du CPU, pour des pertes de performances mineures si bien fait.
De nos jours, le partitionnement est utilisé en complément de la duplication des ALU entières, et de la duplication des autres unités. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. Il est intéressant de regarder comment les processeurs superscalaires ont évolué dans le temps.
==L'évolution historique de la superscalarité==
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. La plupart des CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 sont rapidement montés à de la quadruple émission, avec quelques exceptions. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 ||
|-
! 1995
| 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000
|-
! 1996
| class="f_bleu | 620 || || || || class="f_bleu | 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
===Les processeurs historiques à double émission===
Pour commencer, étudions le cas des premiers CPU superscalaires à double émission. Ils sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Notamment, la duplication des autres unités ne sert pour ainsi dire à rien, ce qui limite les possibilités, mais rendra les explications plus simples.
Il pouvaient se classer en trois types principaux, assez stricts, avec quelques processeurs qui ne rentraient pas dans ces cases. Le premier utilise un ''partitionnement complet'' entre ALU, FPU et unité mémoire. Le second utilisait la double émission entière-flottante. Le troisième utilisait la double émission entière. Voici le tableau chronologique disant quel CPU utilisait quelle technique.
* Le partitionnement INT-FLOAT-MEM est en jaune.
* La double émission entière-flottante est en vert.
* La double émission entière est en rouge.
* Les CPU barrés utilisent la quadruple émission et seront vu dans la section suivante.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| class="f_vert | RS/6000 || || || class="f_rouge | i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| class="f_vert | RSC || class="f_vert | PA 7100 || SuperSPARC || || class="f_jaune | 21064 ||
|-
! 1993
| class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || ||
|-
! 1994
| <s>POWER 2</s> || PA 7200 || || || <s>21164</s> ||
|}
Vous voyez que certains processeur sont laissés de côté dans le tableau précédent. C'est car ils ne rentrent pas dans les catégories précédentes et faisaient autrement. Si on omet le Motorola 88110, qui utilisait une forme de double émission assez avancée, les deux CPUs qui vont nous intéresser sont le PA-RISC PA 7200 d'HP, et le SuperSPARC.
Le PA 7200, sorti en 1994, est un PA-7100 auquel on aurait rajouté une ALU entière. Précisons que ce processeur exécutait les multiplications entières dans la FPU, comme son prédécesseur. Les combinaisons autorisées étaient donc INT + INT, INT + MEM, INT + FLOAT/MUL, MEM + FLOAT/MUL. Les combinaisons autorisées par le partitionnement INT-FLOAT-MEM étaient donc possibles, en plus des combinaisons INT + INT et INT + INT + MUL. Autrement dit, il pouvait faire toute ce que permettait un CPU à double émission entière, plus les combinaisons FLOAT/MUL + MEM.
Le SuperSPARC était un CPU triple émission, avec deux ALU entières, une FPU et une unité mémoire. Il pouvait émettre, à chaque cycle, trois µops en piochant dans deux opérations entières, un accès mémoire, une opération flottante. Le SuperSPARC avait de nombreuses contraintes d’appariement. Par exemple, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Une bonne partie de ces contraintes était lié au banc de registre, qui n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions.
: La seconde ALU a été abandonnée dans l'HyperSPARC, qui est de plus repassé en double émission, ce qui est une évolution à rebours de l'évolution des autres CPUs. Le SuperSPARC était en avance sur son temps, ce quelques années, alors que l'HyperSPARC a un design de son temps.
===Les processeurs historiques à quadruple émission===
C'est la même année , en 1994, que la quadruple émission est arrivée et que les CPU ont commencé à avoir de plus en plus d'unités de calcul. Même les designs restés en double émission avaient ajouté des unités de calcul. Il y avait alors trois stratégies pour la quadruple émission.
La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Les FPUs étaient soit dupliquées, soit scindées en deux en séparant l'additionneur du multiplieur flottant. Le POWER 2 dupliquait ses FPUs à la perfection, alors que l'21264 partitionnait la FPU. Les accès mémoire étaient traités comme des opérations entières, et le processeur pouvait : soit en faire un seul par cycle, soit en émettre deux, sous conditions (le cache devait être double port pour lire/écrire deux données à la fois, comme on le verra dans la suite du chapitre).
Les processeurs MIPS R8000 et R10000 allaient un peu plus loin, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités.
La seconde implémentation dupliquait l'ALU entière et combinait le tout avec un partitionnement INT-FLOAT-MEM. De quoi exécuter en même temps deux opérations entières, une opération flottante, et un accès mémoire. 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).
Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, avec au minimum deux ALU entières. Pour comprendre ce que je veux dire, prenons l'exemple de l'UltraSPARC. Il avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplier flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes. Le processeur pouvait émettre 4 instructions, tant qu'elles atterrissaient dans des unités différentes.
La même méthode peut être utilisée pour la double émission. Le Motorola 88110, sorti en 1991, était un CPU à double émission avec pas moins de 10 unités : deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise.
La morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes. Le tout est résumé dans le tableau ci-dessous, avec :
* en vert les CPU utilisant la première stratégie ;
* en rouge les CPU utilisant la seconde stratégie ;
* en jaune les CPU utilisant la troisième stratégie.
{|class="wikitable"
|+ Processeurs quadruple émission historiques
|-
! Année !! Processeur !! Unités
|-
! 1991
| class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques
|-
|-
! rowspan="2" | 1994
| class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'')
|-
| class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM
|-
! rowspan="3" | 1995
| class="f_jaune | PA 8000 || class="f_jaune | 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD
|-
| class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques
|-
| MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM
|-
! rowspan="2" | 1996
| class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM
|-
| class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL)
|}
===Les processeurs superscalaires modernes===
Les CPU superscalaires modernes ont des design légèrement différents des CPU historiques. Ils dupliquent l'unité entière, mais leur partitionnement est différent. En général, les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Par exemple, un CPU à 6 voies pourra émettre/exécuter 6 opérations entières simultanément, grâce à 6 ALU entières. Mais il s'agit là d'un maximum et certaines peuvent être remplacées par une opération flottante ou mémoire, qui utilisent l'unité mémoire et/ou la FPU.
Un exemple assez parlant est les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon. C'était une microarchitecture à triple émission, avec trois ALU entières et trois unités mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements.
Il s'agit là d'une tendance, cependant. Les CPU superscalaires tendent à mettre le paquet niveau opérations arithmétiques, mais pas forcément au point de remplir la totalité de la largeur du processeur. La règle est que pour N voies, le processeur dispose d'au moins N/2 ALU entières. Attention, cela ne signifie pas que le processeur émet systématiquement N/2 opération entières. Il peut par exemple émettre une combinaison d'opérations flottantes et mémoire, avec des branchements. Et N/2 est une borne basse, la plupart des CPU x86 ont environ tournent plutôt autour des 2/3, voire des 3/4.
En général, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. La conséquence est que les unités mémoire sont dupliquées, en seconde place dans l'ordre d'importance. Les processeurs x86 ont commencé à dupliquer leurs unités mémoire dès qu'ils ont atteint la quadruple émission, pour les CPU grand public. La microarchitecture Core d'Intel utilisait la quadruple émission, avec deux unités mémoire et trois ALU entières. Les microarchitectures d'AMD K5 et K6 utilisaient aussi la quadruple émission et avaient deux ALUs pour deux unités mémoire.
Plus récemment, la microarchitecture ''Golden Cove'' dispose de cinq unités de calcul trois unités mémoire (deux spécialisées dans les écritures, trois dans les lectures). La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire.Il y a donc soit égalité, soit avantage pour les ALU entières.
Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Le banc de registre d'un processeur superscalaire===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Une autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage.
===Les unités d'exécution dans le désordre===
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions auto-aligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions auto-aligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez longs, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
3673m97no5fh8k611v1pgbqu2x624c9
773170
773169
2026-09-25T20:03:00Z
Mewtow
31375
/* L'évolution historique de la superscalarité */
773170
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===L'impact des contraintes d’appariement sur la performance===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour la suite du chapitre===
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==Le ''front-end'' des processeurs superscalaires==
Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple.
Intuitivement, charger, décoder et 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 certaines processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. 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
|}
Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs.
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===L'émission parallèle des instructions===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elle sont propagée au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage).
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
==Les processeurs implémentés avec plusieurs pipelines==
Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Vous vous dites que l'implémentation d'un processeur superscalaire n'est pas la même suivant que le CPU utilise l'exécution dans le désordre, ou non. Mais dans les faits, la distinction est plus subtile que ca. Pour l'expliquer, je vais utiliser une classification originale, qui sépare les processeurs superscalaires en trois sous-types. La distinction se fait sur la manière dont la superscalarité est implémentée en matériel.La classification que j'utilise distingue :
* Les processeurs avec plusieurs pipelines, sans exécution dans le désordre.
* Les processeurs superscalaires avec un pipeline dynamique.
* Les processeurs avec des fenêtres d'instruction décentralisées.
La distinction n'est pas qu'une question d'implémentation matérielle. Elle impacte aussi le ratio entre instructions chargées/décodées et instructions émises. Vous vous dites que les deux sont égales, mais ce n'est le cas que sur les processeurs avec N pipelines ! Sur les autres processeurs, il est possible de décoder 4 instructions, mais d'en émettre maximum 6 par cycle ! Et sur les CPU avec des fenêtres d'instruction décentralisées, il y a aussi une différence entre le nombre d'instructions qui sont ''dispatch'' et ''schedule''.
La catégorie la plus simple est celle des '''processeurs avec plusieurs pipelines'''. Ce sont des processeurs superscalaires sans exécution dans le désordre. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction.
Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, d'une FPU, éventuellement d'un second banc de registres, guère plus. Il permet d'exécuter une seconde opération de calcul, pas plus, ce qui est à l'origine de contraintes d'appariemment assez fortes. De plus, il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section.
===La double émission entière-flottante===
L'exemple le plus simple utilise un pipeline pour les opérations flottantes, séparé du pipeline originel. Pour bien expliquer les choses, prenons le processeur illustré ci-dessous. Un processeur a deux ''back-end'' séparés, un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par l'ALU entière, passent éventuellement dans l'unité mémoire, pour ensuite arriver à l'étape de ''writeback''. La FPU et le banc de registre flottant ont eux aussi leur propre ''back-end'' séparé.
[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]]
Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaqaue pipeline, à chaque cycle.
L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine.
Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants.
===La double émission entière===
Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait.
Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
==Les processeurs superscalaires dynamiques==
Les processeurs précédents, avec 2 pipelines, avaient une unité d'émission assez simple. Elle avait deux ports de décodage, et deux ports d'émission. Le nombre de ports de décodage et d'émission étaient identiques. Vous vous dites que c'est naturel, et intuitif, que c'est normal pour un processeur superscalaire. Mais en réalité, la majorité des processeurs superscalaires a plus de ports d'émission que de décodage. Expliquer pourquoi va cependant nous demander de parler de comment la superscalarité exploite les unités de calcul.
Posons-nous cette question : de combien d'unités un processeur superscalaire a besoin ? Les unités en question ne sont nécessairement des unité de calcul, l'unité mémoire et l'unité de branchement comptent aussi. La réponse dépend du nombre de voies. Si le ''front-end'' charge et décode un paquet de N instructions, exécuter ces N instructions simultanément demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section.
La réponse dépend aussi des contraintes d’appariement. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission parfaite devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement, ce qui ferait passer de 6 unités à 12. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, le cout en circuit encore plus prohibitif !
En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée, alors que la double émission parait plus adaptée.
Et c'est là que la différence entre ports d'émission et de décodage devient intéressante. L'idée est d'avoir une unité d'émission avec deux ports de décodage, et 4 port d'émission (un par unité). Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux micro-opérations tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu.
Les tout premiers CPU superscalaires étaient nombreux à utiliser cette technique. Les exemples classiques sont l'HyperSPARC, de l'alpha 21064 et du PowerPC 603. C'étaient des CPU double émission, avec une ALU entière, une unité mémoire et une FPU. Nous parlerons de '''partitionnement INT-FLOAT-MEM'''. Les contraintes d’appariement variaient grandement suivant le processeur.
Le PowerPC 603 implémentait une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions).
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple, facile à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. La raison est que le partitionnement permet d'émettre des paires, triplets, quadruplets de micro-opérations qui sont rares en pratique.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''.
Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, 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. Et enfin, il faut rajouter un port d'émission pour pouvoir émettre une micro-opération entière en plus.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle.
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
Reste à connecter les unités de calcul à l'unité d'émission (indirectement, car il y a des étages de pipeline entre les deux). Une implémentation basique utilisee un port d'émission par unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La présence d'un circuit multiplieur et d'un ''barrel shifter'' pose des problèmes pour le partitionnement. Il est possible d'avoir un port d'émission dédié au multiplieur, et un autre au ''barrel shifter'', qui sont séparés de ceux pour l'ALU entière. Il est alors possible d'émettre une opération entière, une multiplication et un décalage, simultanément. Nous parlerons de '''partitionnement entier''' pour désigner cette possibilité.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
Le partitionnement entier est cependant très rare, car le gain en performance serait trop faible au regard des couts. Quelques processeurs historiques l'utilisaient, mais ce n'est plus le cas de nos jours. La raison est que les multiplications et les décalages sont assez rares dans le code, ce qui fait que le port d'émission associés sont sous-utilisés. Et un port d'émission a un cout en circuit assez important. 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.
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul. L'exemple classique est justement lié aux circuits multiplieur et ''barrel shifters''. L'idée est que le multiplieur et le ''barrel shifter'' sont mis sur le même port d'émission qu'une ALU entière.
Prenons par exemple le processeur à double émission entière illustré ci-contre. Un port d'émission est relié à une ALU entière seule, et l'autre port d'émission alimente la seconde ALU entière et le multiplieur. La conséquence est que le processeur peut émettre deux opérations entières, dont une multiplication. Si on avait utilisé un port d'émission dédié au multiplieur, il aurait été possible d'émettre deux opérations entières et une multiplication. On perd donc une voie, mais on gagne en flexibilité et on économise des circuits.
[[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]]
En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais le partage des ports d'émission peut appliquer à n'importe quelle unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. 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.
La solution est de partager des ports d'émission, pour passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple relie une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires modernes utilisent toutes ces stratégies===
Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autre unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents.
* Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU.
* La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants.
* La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables.
* Le '''partage des ports d'émission''' réduit le cout en circuit du CPU, pour des pertes de performances mineures si bien fait.
De nos jours, le partitionnement est utilisé en complément de la duplication des ALU entières, et de la duplication des autres unités. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. Il est intéressant de regarder comment les processeurs superscalaires ont évolué dans le temps.
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. La plupart des CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 sont rapidement montés à de la quadruple émission, avec quelques exceptions. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 ||
|-
! 1995
| 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000
|-
! 1996
| class="f_bleu | 620 || || || || class="f_bleu | 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
===Les processeurs historiques à double émission===
Pour commencer, étudions le cas des premiers CPU superscalaires à double émission. Ils sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Notamment, la duplication des autres unités ne sert pour ainsi dire à rien, ce qui limite les possibilités, mais rendra les explications plus simples.
Il pouvaient se classer en trois types principaux, assez stricts, avec quelques processeurs qui ne rentraient pas dans ces cases. Le premier utilise un ''partitionnement complet'' entre ALU, FPU et unité mémoire. Le second utilisait la double émission entière-flottante. Le troisième utilisait la double émission entière. Voici le tableau chronologique disant quel CPU utilisait quelle technique.
* Le partitionnement INT-FLOAT-MEM est en jaune.
* La double émission entière-flottante est en vert.
* La double émission entière est en rouge.
* Les CPU barrés utilisent la quadruple émission et seront vu dans la section suivante.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| class="f_vert | RS/6000 || || || class="f_rouge | i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| class="f_vert | RSC || class="f_vert | PA 7100 || SuperSPARC || || class="f_jaune | 21064 ||
|-
! 1993
| class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || ||
|-
! 1994
| <s>POWER 2</s> || PA 7200 || || || <s>21164</s> ||
|}
Vous voyez que certains processeur sont laissés de côté dans le tableau précédent. C'est car ils ne rentrent pas dans les catégories précédentes et faisaient autrement. Si on omet le Motorola 88110, qui utilisait une forme de double émission assez avancée, les deux CPUs qui vont nous intéresser sont le PA-RISC PA 7200 d'HP, et le SuperSPARC.
Le PA 7200, sorti en 1994, est un PA-7100 auquel on aurait rajouté une ALU entière. Précisons que ce processeur exécutait les multiplications entières dans la FPU, comme son prédécesseur. Les combinaisons autorisées étaient donc INT + INT, INT + MEM, INT + FLOAT/MUL, MEM + FLOAT/MUL. Les combinaisons autorisées par le partitionnement INT-FLOAT-MEM étaient donc possibles, en plus des combinaisons INT + INT et INT + INT + MUL. Autrement dit, il pouvait faire toute ce que permettait un CPU à double émission entière, plus les combinaisons FLOAT/MUL + MEM.
Le SuperSPARC était un CPU triple émission, avec deux ALU entières, une FPU et une unité mémoire. Il pouvait émettre, à chaque cycle, trois µops en piochant dans deux opérations entières, un accès mémoire, une opération flottante. Le SuperSPARC avait de nombreuses contraintes d’appariement. Par exemple, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Une bonne partie de ces contraintes était lié au banc de registre, qui n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions.
: La seconde ALU a été abandonnée dans l'HyperSPARC, qui est de plus repassé en double émission, ce qui est une évolution à rebours de l'évolution des autres CPUs. Le SuperSPARC était en avance sur son temps, ce quelques années, alors que l'HyperSPARC a un design de son temps.
===Les processeurs historiques à quadruple émission===
C'est la même année , en 1994, que la quadruple émission est arrivée et que les CPU ont commencé à avoir de plus en plus d'unités de calcul. Même les designs restés en double émission avaient ajouté des unités de calcul. Il y avait alors trois stratégies pour la quadruple émission.
La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Les FPUs étaient soit dupliquées, soit scindées en deux en séparant l'additionneur du multiplieur flottant. Le POWER 2 dupliquait ses FPUs à la perfection, alors que l'21264 partitionnait la FPU. Les accès mémoire étaient traités comme des opérations entières, et le processeur pouvait : soit en faire un seul par cycle, soit en émettre deux, sous conditions (le cache devait être double port pour lire/écrire deux données à la fois, comme on le verra dans la suite du chapitre).
Les processeurs MIPS R8000 et R10000 allaient un peu plus loin, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités.
La seconde implémentation dupliquait l'ALU entière et combinait le tout avec un partitionnement INT-FLOAT-MEM. De quoi exécuter en même temps deux opérations entières, une opération flottante, et un accès mémoire. 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).
Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, avec au minimum deux ALU entières. Pour comprendre ce que je veux dire, prenons l'exemple de l'UltraSPARC. Il avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplier flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes. Le processeur pouvait émettre 4 instructions, tant qu'elles atterrissaient dans des unités différentes.
La même méthode peut être utilisée pour la double émission. Le Motorola 88110, sorti en 1991, était un CPU à double émission avec pas moins de 10 unités : deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise.
La morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes. Le tout est résumé dans le tableau ci-dessous, avec :
* en vert les CPU utilisant la première stratégie ;
* en rouge les CPU utilisant la seconde stratégie ;
* en jaune les CPU utilisant la troisième stratégie.
{|class="wikitable"
|+ Processeurs quadruple émission historiques
|-
! Année !! Processeur !! Unités
|-
! 1991
| class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques
|-
|-
! rowspan="2" | 1994
| class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'')
|-
| class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM
|-
! rowspan="3" | 1995
| class="f_jaune | PA 8000 || class="f_jaune | 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD
|-
| class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques
|-
| MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM
|-
! rowspan="2" | 1996
| class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM
|-
| class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL)
|}
===Les processeurs superscalaires modernes===
Les CPU superscalaires modernes ont des design légèrement différents des CPU historiques. Ils dupliquent l'unité entière, mais leur partitionnement est différent. En général, les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Par exemple, un CPU à 6 voies pourra émettre/exécuter 6 opérations entières simultanément, grâce à 6 ALU entières. Mais il s'agit là d'un maximum et certaines peuvent être remplacées par une opération flottante ou mémoire, qui utilisent l'unité mémoire et/ou la FPU.
Un exemple assez parlant est les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon. C'était une microarchitecture à triple émission, avec trois ALU entières et trois unités mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements.
Il s'agit là d'une tendance, cependant. Les CPU superscalaires tendent à mettre le paquet niveau opérations arithmétiques, mais pas forcément au point de remplir la totalité de la largeur du processeur. La règle est que pour N voies, le processeur dispose d'au moins N/2 ALU entières. Attention, cela ne signifie pas que le processeur émet systématiquement N/2 opération entières. Il peut par exemple émettre une combinaison d'opérations flottantes et mémoire, avec des branchements. Et N/2 est une borne basse, la plupart des CPU x86 ont environ tournent plutôt autour des 2/3, voire des 3/4.
En général, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. La conséquence est que les unités mémoire sont dupliquées, en seconde place dans l'ordre d'importance. Les processeurs x86 ont commencé à dupliquer leurs unités mémoire dès qu'ils ont atteint la quadruple émission, pour les CPU grand public. La microarchitecture Core d'Intel utilisait la quadruple émission, avec deux unités mémoire et trois ALU entières. Les microarchitectures d'AMD K5 et K6 utilisaient aussi la quadruple émission et avaient deux ALUs pour deux unités mémoire.
Plus récemment, la microarchitecture ''Golden Cove'' dispose de cinq unités de calcul trois unités mémoire (deux spécialisées dans les écritures, trois dans les lectures). La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire.Il y a donc soit égalité, soit avantage pour les ALU entières.
Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Le banc de registre d'un processeur superscalaire===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Une autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage.
===Les unités d'exécution dans le désordre===
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions auto-aligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions auto-aligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez longs, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
sun8mi21y8bidzx7ae06x5scrgq6bs5
773171
773170
2026-09-25T20:03:08Z
Mewtow
31375
/* Les CPU superscalaires modernes utilisent toutes ces stratégies */
773171
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===L'impact des contraintes d’appariement sur la performance===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour la suite du chapitre===
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==Le ''front-end'' des processeurs superscalaires==
Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple.
Intuitivement, charger, décoder et 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 certaines processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. 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
|}
Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs.
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===L'émission parallèle des instructions===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elle sont propagée au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage).
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
==Les processeurs implémentés avec plusieurs pipelines==
Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Vous vous dites que l'implémentation d'un processeur superscalaire n'est pas la même suivant que le CPU utilise l'exécution dans le désordre, ou non. Mais dans les faits, la distinction est plus subtile que ca. Pour l'expliquer, je vais utiliser une classification originale, qui sépare les processeurs superscalaires en trois sous-types. La distinction se fait sur la manière dont la superscalarité est implémentée en matériel.La classification que j'utilise distingue :
* Les processeurs avec plusieurs pipelines, sans exécution dans le désordre.
* Les processeurs superscalaires avec un pipeline dynamique.
* Les processeurs avec des fenêtres d'instruction décentralisées.
La distinction n'est pas qu'une question d'implémentation matérielle. Elle impacte aussi le ratio entre instructions chargées/décodées et instructions émises. Vous vous dites que les deux sont égales, mais ce n'est le cas que sur les processeurs avec N pipelines ! Sur les autres processeurs, il est possible de décoder 4 instructions, mais d'en émettre maximum 6 par cycle ! Et sur les CPU avec des fenêtres d'instruction décentralisées, il y a aussi une différence entre le nombre d'instructions qui sont ''dispatch'' et ''schedule''.
La catégorie la plus simple est celle des '''processeurs avec plusieurs pipelines'''. Ce sont des processeurs superscalaires sans exécution dans le désordre. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction.
Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, d'une FPU, éventuellement d'un second banc de registres, guère plus. Il permet d'exécuter une seconde opération de calcul, pas plus, ce qui est à l'origine de contraintes d'appariemment assez fortes. De plus, il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section.
===La double émission entière-flottante===
L'exemple le plus simple utilise un pipeline pour les opérations flottantes, séparé du pipeline originel. Pour bien expliquer les choses, prenons le processeur illustré ci-dessous. Un processeur a deux ''back-end'' séparés, un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par l'ALU entière, passent éventuellement dans l'unité mémoire, pour ensuite arriver à l'étape de ''writeback''. La FPU et le banc de registre flottant ont eux aussi leur propre ''back-end'' séparé.
[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]]
Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaqaue pipeline, à chaque cycle.
L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine.
Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants.
===La double émission entière===
Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait.
Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
==Les processeurs superscalaires dynamiques==
Les processeurs précédents, avec 2 pipelines, avaient une unité d'émission assez simple. Elle avait deux ports de décodage, et deux ports d'émission. Le nombre de ports de décodage et d'émission étaient identiques. Vous vous dites que c'est naturel, et intuitif, que c'est normal pour un processeur superscalaire. Mais en réalité, la majorité des processeurs superscalaires a plus de ports d'émission que de décodage. Expliquer pourquoi va cependant nous demander de parler de comment la superscalarité exploite les unités de calcul.
Posons-nous cette question : de combien d'unités un processeur superscalaire a besoin ? Les unités en question ne sont nécessairement des unité de calcul, l'unité mémoire et l'unité de branchement comptent aussi. La réponse dépend du nombre de voies. Si le ''front-end'' charge et décode un paquet de N instructions, exécuter ces N instructions simultanément demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section.
La réponse dépend aussi des contraintes d’appariement. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission parfaite devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement, ce qui ferait passer de 6 unités à 12. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, le cout en circuit encore plus prohibitif !
En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée, alors que la double émission parait plus adaptée.
Et c'est là que la différence entre ports d'émission et de décodage devient intéressante. L'idée est d'avoir une unité d'émission avec deux ports de décodage, et 4 port d'émission (un par unité). Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux micro-opérations tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu.
Les tout premiers CPU superscalaires étaient nombreux à utiliser cette technique. Les exemples classiques sont l'HyperSPARC, de l'alpha 21064 et du PowerPC 603. C'étaient des CPU double émission, avec une ALU entière, une unité mémoire et une FPU. Nous parlerons de '''partitionnement INT-FLOAT-MEM'''. Les contraintes d’appariement variaient grandement suivant le processeur.
Le PowerPC 603 implémentait une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions).
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple, facile à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. La raison est que le partitionnement permet d'émettre des paires, triplets, quadruplets de micro-opérations qui sont rares en pratique.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''.
Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, 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. Et enfin, il faut rajouter un port d'émission pour pouvoir émettre une micro-opération entière en plus.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle.
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
Reste à connecter les unités de calcul à l'unité d'émission (indirectement, car il y a des étages de pipeline entre les deux). Une implémentation basique utilisee un port d'émission par unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La présence d'un circuit multiplieur et d'un ''barrel shifter'' pose des problèmes pour le partitionnement. Il est possible d'avoir un port d'émission dédié au multiplieur, et un autre au ''barrel shifter'', qui sont séparés de ceux pour l'ALU entière. Il est alors possible d'émettre une opération entière, une multiplication et un décalage, simultanément. Nous parlerons de '''partitionnement entier''' pour désigner cette possibilité.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
Le partitionnement entier est cependant très rare, car le gain en performance serait trop faible au regard des couts. Quelques processeurs historiques l'utilisaient, mais ce n'est plus le cas de nos jours. La raison est que les multiplications et les décalages sont assez rares dans le code, ce qui fait que le port d'émission associés sont sous-utilisés. Et un port d'émission a un cout en circuit assez important. 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.
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul. L'exemple classique est justement lié aux circuits multiplieur et ''barrel shifters''. L'idée est que le multiplieur et le ''barrel shifter'' sont mis sur le même port d'émission qu'une ALU entière.
Prenons par exemple le processeur à double émission entière illustré ci-contre. Un port d'émission est relié à une ALU entière seule, et l'autre port d'émission alimente la seconde ALU entière et le multiplieur. La conséquence est que le processeur peut émettre deux opérations entières, dont une multiplication. Si on avait utilisé un port d'émission dédié au multiplieur, il aurait été possible d'émettre deux opérations entières et une multiplication. On perd donc une voie, mais on gagne en flexibilité et on économise des circuits.
[[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]]
En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais le partage des ports d'émission peut appliquer à n'importe quelle unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. 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.
La solution est de partager des ports d'émission, pour passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple relie une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
==L'évolution historique de la superscalarité==
Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autre unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents.
* Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU.
* La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants.
* La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables.
* Le '''partage des ports d'émission''' réduit le cout en circuit du CPU, pour des pertes de performances mineures si bien fait.
De nos jours, le partitionnement est utilisé en complément de la duplication des ALU entières, et de la duplication des autres unités. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. Il est intéressant de regarder comment les processeurs superscalaires ont évolué dans le temps.
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. La plupart des CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 sont rapidement montés à de la quadruple émission, avec quelques exceptions. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 ||
|-
! 1995
| 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000
|-
! 1996
| class="f_bleu | 620 || || || || class="f_bleu | 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
===Les processeurs historiques à double émission===
Pour commencer, étudions le cas des premiers CPU superscalaires à double émission. Ils sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Notamment, la duplication des autres unités ne sert pour ainsi dire à rien, ce qui limite les possibilités, mais rendra les explications plus simples.
Il pouvaient se classer en trois types principaux, assez stricts, avec quelques processeurs qui ne rentraient pas dans ces cases. Le premier utilise un ''partitionnement complet'' entre ALU, FPU et unité mémoire. Le second utilisait la double émission entière-flottante. Le troisième utilisait la double émission entière. Voici le tableau chronologique disant quel CPU utilisait quelle technique.
* Le partitionnement INT-FLOAT-MEM est en jaune.
* La double émission entière-flottante est en vert.
* La double émission entière est en rouge.
* Les CPU barrés utilisent la quadruple émission et seront vu dans la section suivante.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| class="f_vert | RS/6000 || || || class="f_rouge | i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| class="f_vert | RSC || class="f_vert | PA 7100 || SuperSPARC || || class="f_jaune | 21064 ||
|-
! 1993
| class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || ||
|-
! 1994
| <s>POWER 2</s> || PA 7200 || || || <s>21164</s> ||
|}
Vous voyez que certains processeur sont laissés de côté dans le tableau précédent. C'est car ils ne rentrent pas dans les catégories précédentes et faisaient autrement. Si on omet le Motorola 88110, qui utilisait une forme de double émission assez avancée, les deux CPUs qui vont nous intéresser sont le PA-RISC PA 7200 d'HP, et le SuperSPARC.
Le PA 7200, sorti en 1994, est un PA-7100 auquel on aurait rajouté une ALU entière. Précisons que ce processeur exécutait les multiplications entières dans la FPU, comme son prédécesseur. Les combinaisons autorisées étaient donc INT + INT, INT + MEM, INT + FLOAT/MUL, MEM + FLOAT/MUL. Les combinaisons autorisées par le partitionnement INT-FLOAT-MEM étaient donc possibles, en plus des combinaisons INT + INT et INT + INT + MUL. Autrement dit, il pouvait faire toute ce que permettait un CPU à double émission entière, plus les combinaisons FLOAT/MUL + MEM.
Le SuperSPARC était un CPU triple émission, avec deux ALU entières, une FPU et une unité mémoire. Il pouvait émettre, à chaque cycle, trois µops en piochant dans deux opérations entières, un accès mémoire, une opération flottante. Le SuperSPARC avait de nombreuses contraintes d’appariement. Par exemple, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Une bonne partie de ces contraintes était lié au banc de registre, qui n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions.
: La seconde ALU a été abandonnée dans l'HyperSPARC, qui est de plus repassé en double émission, ce qui est une évolution à rebours de l'évolution des autres CPUs. Le SuperSPARC était en avance sur son temps, ce quelques années, alors que l'HyperSPARC a un design de son temps.
===Les processeurs historiques à quadruple émission===
C'est la même année , en 1994, que la quadruple émission est arrivée et que les CPU ont commencé à avoir de plus en plus d'unités de calcul. Même les designs restés en double émission avaient ajouté des unités de calcul. Il y avait alors trois stratégies pour la quadruple émission.
La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Les FPUs étaient soit dupliquées, soit scindées en deux en séparant l'additionneur du multiplieur flottant. Le POWER 2 dupliquait ses FPUs à la perfection, alors que l'21264 partitionnait la FPU. Les accès mémoire étaient traités comme des opérations entières, et le processeur pouvait : soit en faire un seul par cycle, soit en émettre deux, sous conditions (le cache devait être double port pour lire/écrire deux données à la fois, comme on le verra dans la suite du chapitre).
Les processeurs MIPS R8000 et R10000 allaient un peu plus loin, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités.
La seconde implémentation dupliquait l'ALU entière et combinait le tout avec un partitionnement INT-FLOAT-MEM. De quoi exécuter en même temps deux opérations entières, une opération flottante, et un accès mémoire. 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).
Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, avec au minimum deux ALU entières. Pour comprendre ce que je veux dire, prenons l'exemple de l'UltraSPARC. Il avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplier flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes. Le processeur pouvait émettre 4 instructions, tant qu'elles atterrissaient dans des unités différentes.
La même méthode peut être utilisée pour la double émission. Le Motorola 88110, sorti en 1991, était un CPU à double émission avec pas moins de 10 unités : deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise.
La morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes. Le tout est résumé dans le tableau ci-dessous, avec :
* en vert les CPU utilisant la première stratégie ;
* en rouge les CPU utilisant la seconde stratégie ;
* en jaune les CPU utilisant la troisième stratégie.
{|class="wikitable"
|+ Processeurs quadruple émission historiques
|-
! Année !! Processeur !! Unités
|-
! 1991
| class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques
|-
|-
! rowspan="2" | 1994
| class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'')
|-
| class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM
|-
! rowspan="3" | 1995
| class="f_jaune | PA 8000 || class="f_jaune | 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD
|-
| class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques
|-
| MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM
|-
! rowspan="2" | 1996
| class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM
|-
| class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL)
|}
===Les processeurs superscalaires modernes===
Les CPU superscalaires modernes ont des design légèrement différents des CPU historiques. Ils dupliquent l'unité entière, mais leur partitionnement est différent. En général, les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Par exemple, un CPU à 6 voies pourra émettre/exécuter 6 opérations entières simultanément, grâce à 6 ALU entières. Mais il s'agit là d'un maximum et certaines peuvent être remplacées par une opération flottante ou mémoire, qui utilisent l'unité mémoire et/ou la FPU.
Un exemple assez parlant est les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon. C'était une microarchitecture à triple émission, avec trois ALU entières et trois unités mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements.
Il s'agit là d'une tendance, cependant. Les CPU superscalaires tendent à mettre le paquet niveau opérations arithmétiques, mais pas forcément au point de remplir la totalité de la largeur du processeur. La règle est que pour N voies, le processeur dispose d'au moins N/2 ALU entières. Attention, cela ne signifie pas que le processeur émet systématiquement N/2 opération entières. Il peut par exemple émettre une combinaison d'opérations flottantes et mémoire, avec des branchements. Et N/2 est une borne basse, la plupart des CPU x86 ont environ tournent plutôt autour des 2/3, voire des 3/4.
En général, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. La conséquence est que les unités mémoire sont dupliquées, en seconde place dans l'ordre d'importance. Les processeurs x86 ont commencé à dupliquer leurs unités mémoire dès qu'ils ont atteint la quadruple émission, pour les CPU grand public. La microarchitecture Core d'Intel utilisait la quadruple émission, avec deux unités mémoire et trois ALU entières. Les microarchitectures d'AMD K5 et K6 utilisaient aussi la quadruple émission et avaient deux ALUs pour deux unités mémoire.
Plus récemment, la microarchitecture ''Golden Cove'' dispose de cinq unités de calcul trois unités mémoire (deux spécialisées dans les écritures, trois dans les lectures). La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire.Il y a donc soit égalité, soit avantage pour les ALU entières.
Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Le banc de registre d'un processeur superscalaire===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Une autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage.
===Les unités d'exécution dans le désordre===
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions auto-aligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions auto-aligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez longs, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
21viekt6i4i0nq6rnsu5dzpj3ud25on
773172
773171
2026-09-25T20:09:59Z
Mewtow
31375
/* Les processeurs superscalaires dynamiques */
773172
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===L'impact des contraintes d’appariement sur la performance===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour la suite du chapitre===
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==Le ''front-end'' des processeurs superscalaires==
Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple.
Intuitivement, charger, décoder et 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 certaines processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. 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
|}
Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs.
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===L'émission parallèle des instructions===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elle sont propagée au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage).
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
==Les processeurs implémentés avec plusieurs pipelines==
Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Vous vous dites que l'implémentation d'un processeur superscalaire n'est pas la même suivant que le CPU utilise l'exécution dans le désordre, ou non. Mais dans les faits, la distinction est plus subtile que ca. Pour l'expliquer, je vais utiliser une classification originale, qui sépare les processeurs superscalaires en trois sous-types. La distinction se fait sur la manière dont la superscalarité est implémentée en matériel.La classification que j'utilise distingue :
* Les processeurs avec plusieurs pipelines, sans exécution dans le désordre.
* Les processeurs superscalaires avec un pipeline dynamique.
* Les processeurs avec des fenêtres d'instruction décentralisées.
La distinction n'est pas qu'une question d'implémentation matérielle. Elle impacte aussi le ratio entre instructions chargées/décodées et instructions émises. Vous vous dites que les deux sont égales, mais ce n'est le cas que sur les processeurs avec N pipelines ! Sur les autres processeurs, il est possible de décoder 4 instructions, mais d'en émettre maximum 6 par cycle ! Et sur les CPU avec des fenêtres d'instruction décentralisées, il y a aussi une différence entre le nombre d'instructions qui sont ''dispatch'' et ''schedule''.
La catégorie la plus simple est celle des '''processeurs avec plusieurs pipelines'''. Ce sont des processeurs superscalaires sans exécution dans le désordre. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction.
Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, d'une FPU, éventuellement d'un second banc de registres, guère plus. Il permet d'exécuter une seconde opération de calcul, pas plus, ce qui est à l'origine de contraintes d'appariemment assez fortes. De plus, il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section.
===La double émission entière-flottante===
L'exemple le plus simple utilise un pipeline pour les opérations flottantes, séparé du pipeline originel. Pour bien expliquer les choses, prenons le processeur illustré ci-dessous. Un processeur a deux ''back-end'' séparés, un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par l'ALU entière, passent éventuellement dans l'unité mémoire, pour ensuite arriver à l'étape de ''writeback''. La FPU et le banc de registre flottant ont eux aussi leur propre ''back-end'' séparé.
[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]]
Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaqaue pipeline, à chaque cycle.
L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine.
Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants.
===La double émission entière===
Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait.
Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
==Les processeurs superscalaires dynamiques==
Les processeurs précédents, avec 2 pipelines, avaient une unité d'émission assez simple. Elle avait deux ports de décodage, et deux ports d'émission. Le nombre de ports de décodage et d'émission étaient identiques. Vous vous dites que c'est naturel, et intuitif, que c'est normal pour un processeur superscalaire. Mais en réalité, la majorité des processeurs superscalaires a plus de ports d'émission que de décodage. Expliquer pourquoi va cependant nous demander de parler de comment la superscalarité exploite les unités de calcul.
Posons-nous cette question : de combien d'unités un processeur superscalaire a besoin ? Les unités en question ne sont nécessairement des unité de calcul, l'unité mémoire et l'unité de branchement comptent aussi. La réponse dépend du nombre de voies. Si le ''front-end'' charge et décode un paquet de N instructions, exécuter ces N instructions simultanément demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section.
La réponse dépend aussi des contraintes d’appariement. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission parfaite devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement, ce qui ferait passer de 6 unités à 12. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, le cout en circuit encore plus prohibitif !
En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée, alors que la double émission parait plus adaptée.
Et c'est là que la différence entre ports d'émission et de décodage devient intéressante. L'idée est d'avoir une unité d'émission avec deux ports de décodage, et 4 port d'émission (un par unité). Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux micro-opérations tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu.
Les tout premiers CPU superscalaires étaient nombreux à utiliser cette technique. Les exemples classiques sont l'HyperSPARC, de l'alpha 21064 et du PowerPC 603. C'étaient des CPU double émission, avec une ALU entière, une unité mémoire et une FPU. Nous parlerons de '''partitionnement INT-FLOAT-MEM'''. Les contraintes d’appariement variaient grandement suivant le processeur.
Le PowerPC 603 implémentait une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur.
Avant de poursuivre, précisons que la FPU regroupe un additionneur flottant et un multiplieur flottant. Et ce fait peut être exploité pour approfondir le partitionnement. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions).
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple, facile à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. La raison est que le partitionnement permet d'émettre des paires, triplets, quadruplets de micro-opérations qui sont rares en pratique.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''.
Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, 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. Et enfin, il faut rajouter un port d'émission pour pouvoir émettre une micro-opération entière en plus.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle.
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
Reste à connecter les unités de calcul à l'unité d'émission (indirectement, car il y a des étages de pipeline entre les deux). Une implémentation basique utilisee un port d'émission par unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La présence d'un circuit multiplieur et d'un ''barrel shifter'' pose des problèmes pour le partitionnement. Il est possible d'avoir un port d'émission dédié au multiplieur, et un autre au ''barrel shifter'', qui sont séparés de ceux pour l'ALU entière. Il est alors possible d'émettre une opération entière, une multiplication et un décalage, simultanément. Nous parlerons de '''partitionnement entier''' pour désigner cette possibilité.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
Le partitionnement entier est cependant très rare, car le gain en performance serait trop faible au regard des couts. Quelques processeurs historiques l'utilisaient, mais ce n'est plus le cas de nos jours. La raison est que les multiplications et les décalages sont assez rares dans le code, ce qui fait que le port d'émission associés sont sous-utilisés. Et un port d'émission a un cout en circuit assez important. 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.
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul. L'exemple classique est justement lié aux circuits multiplieur et ''barrel shifters''. L'idée est que le multiplieur et le ''barrel shifter'' sont mis sur le même port d'émission qu'une ALU entière.
Prenons par exemple le processeur à double émission entière illustré ci-contre. Un port d'émission est relié à une ALU entière seule, et l'autre port d'émission alimente la seconde ALU entière et le multiplieur. La conséquence est que le processeur peut émettre deux opérations entières, dont une multiplication. Si on avait utilisé un port d'émission dédié au multiplieur, il aurait été possible d'émettre deux opérations entières et une multiplication. On perd donc une voie, mais on gagne en flexibilité et on économise des circuits.
[[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]]
En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais le partage des ports d'émission peut appliquer à n'importe quelle unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. 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.
La solution est de partager des ports d'émission, pour passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple relie une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
==L'évolution historique de la superscalarité==
Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autre unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents.
* Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU.
* La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants.
* La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables.
* Le '''partage des ports d'émission''' réduit le cout en circuit du CPU, pour des pertes de performances mineures si bien fait.
De nos jours, le partitionnement est utilisé en complément de la duplication des ALU entières, et de la duplication des autres unités. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. Il est intéressant de regarder comment les processeurs superscalaires ont évolué dans le temps.
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. La plupart des CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 sont rapidement montés à de la quadruple émission, avec quelques exceptions. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 ||
|-
! 1995
| 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000
|-
! 1996
| class="f_bleu | 620 || || || || class="f_bleu | 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
===Les processeurs historiques à double émission===
Pour commencer, étudions le cas des premiers CPU superscalaires à double émission. Ils sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Notamment, la duplication des autres unités ne sert pour ainsi dire à rien, ce qui limite les possibilités, mais rendra les explications plus simples.
Il pouvaient se classer en trois types principaux, assez stricts, avec quelques processeurs qui ne rentraient pas dans ces cases. Le premier utilise un ''partitionnement complet'' entre ALU, FPU et unité mémoire. Le second utilisait la double émission entière-flottante. Le troisième utilisait la double émission entière. Voici le tableau chronologique disant quel CPU utilisait quelle technique.
* Le partitionnement INT-FLOAT-MEM est en jaune.
* La double émission entière-flottante est en vert.
* La double émission entière est en rouge.
* Les CPU barrés utilisent la quadruple émission et seront vu dans la section suivante.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| class="f_vert | RS/6000 || || || class="f_rouge | i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| class="f_vert | RSC || class="f_vert | PA 7100 || SuperSPARC || || class="f_jaune | 21064 ||
|-
! 1993
| class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || ||
|-
! 1994
| <s>POWER 2</s> || PA 7200 || || || <s>21164</s> ||
|}
Vous voyez que certains processeur sont laissés de côté dans le tableau précédent. C'est car ils ne rentrent pas dans les catégories précédentes et faisaient autrement. Si on omet le Motorola 88110, qui utilisait une forme de double émission assez avancée, les deux CPUs qui vont nous intéresser sont le PA-RISC PA 7200 d'HP, et le SuperSPARC.
Le PA 7200, sorti en 1994, est un PA-7100 auquel on aurait rajouté une ALU entière. Précisons que ce processeur exécutait les multiplications entières dans la FPU, comme son prédécesseur. Les combinaisons autorisées étaient donc INT + INT, INT + MEM, INT + FLOAT/MUL, MEM + FLOAT/MUL. Les combinaisons autorisées par le partitionnement INT-FLOAT-MEM étaient donc possibles, en plus des combinaisons INT + INT et INT + INT + MUL. Autrement dit, il pouvait faire toute ce que permettait un CPU à double émission entière, plus les combinaisons FLOAT/MUL + MEM.
Le SuperSPARC était un CPU triple émission, avec deux ALU entières, une FPU et une unité mémoire. Il pouvait émettre, à chaque cycle, trois µops en piochant dans deux opérations entières, un accès mémoire, une opération flottante. Le SuperSPARC avait de nombreuses contraintes d’appariement. Par exemple, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Une bonne partie de ces contraintes était lié au banc de registre, qui n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions.
: La seconde ALU a été abandonnée dans l'HyperSPARC, qui est de plus repassé en double émission, ce qui est une évolution à rebours de l'évolution des autres CPUs. Le SuperSPARC était en avance sur son temps, ce quelques années, alors que l'HyperSPARC a un design de son temps.
===Les processeurs historiques à quadruple émission===
C'est la même année , en 1994, que la quadruple émission est arrivée et que les CPU ont commencé à avoir de plus en plus d'unités de calcul. Même les designs restés en double émission avaient ajouté des unités de calcul. Il y avait alors trois stratégies pour la quadruple émission.
La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Les FPUs étaient soit dupliquées, soit scindées en deux en séparant l'additionneur du multiplieur flottant. Le POWER 2 dupliquait ses FPUs à la perfection, alors que l'21264 partitionnait la FPU. Les accès mémoire étaient traités comme des opérations entières, et le processeur pouvait : soit en faire un seul par cycle, soit en émettre deux, sous conditions (le cache devait être double port pour lire/écrire deux données à la fois, comme on le verra dans la suite du chapitre).
Les processeurs MIPS R8000 et R10000 allaient un peu plus loin, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités.
La seconde implémentation dupliquait l'ALU entière et combinait le tout avec un partitionnement INT-FLOAT-MEM. De quoi exécuter en même temps deux opérations entières, une opération flottante, et un accès mémoire. 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).
Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, avec au minimum deux ALU entières. Pour comprendre ce que je veux dire, prenons l'exemple de l'UltraSPARC. Il avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplier flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes. Le processeur pouvait émettre 4 instructions, tant qu'elles atterrissaient dans des unités différentes.
La même méthode peut être utilisée pour la double émission. Le Motorola 88110, sorti en 1991, était un CPU à double émission avec pas moins de 10 unités : deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise.
La morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes. Le tout est résumé dans le tableau ci-dessous, avec :
* en vert les CPU utilisant la première stratégie ;
* en rouge les CPU utilisant la seconde stratégie ;
* en jaune les CPU utilisant la troisième stratégie.
{|class="wikitable"
|+ Processeurs quadruple émission historiques
|-
! Année !! Processeur !! Unités
|-
! 1991
| class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques
|-
|-
! rowspan="2" | 1994
| class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'')
|-
| class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM
|-
! rowspan="3" | 1995
| class="f_jaune | PA 8000 || class="f_jaune | 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD
|-
| class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques
|-
| MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM
|-
! rowspan="2" | 1996
| class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM
|-
| class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL)
|}
===Les processeurs superscalaires modernes===
Les CPU superscalaires modernes ont des design légèrement différents des CPU historiques. Ils dupliquent l'unité entière, mais leur partitionnement est différent. En général, les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Par exemple, un CPU à 6 voies pourra émettre/exécuter 6 opérations entières simultanément, grâce à 6 ALU entières. Mais il s'agit là d'un maximum et certaines peuvent être remplacées par une opération flottante ou mémoire, qui utilisent l'unité mémoire et/ou la FPU.
Un exemple assez parlant est les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon. C'était une microarchitecture à triple émission, avec trois ALU entières et trois unités mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements.
Il s'agit là d'une tendance, cependant. Les CPU superscalaires tendent à mettre le paquet niveau opérations arithmétiques, mais pas forcément au point de remplir la totalité de la largeur du processeur. La règle est que pour N voies, le processeur dispose d'au moins N/2 ALU entières. Attention, cela ne signifie pas que le processeur émet systématiquement N/2 opération entières. Il peut par exemple émettre une combinaison d'opérations flottantes et mémoire, avec des branchements. Et N/2 est une borne basse, la plupart des CPU x86 ont environ tournent plutôt autour des 2/3, voire des 3/4.
En général, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. La conséquence est que les unités mémoire sont dupliquées, en seconde place dans l'ordre d'importance. Les processeurs x86 ont commencé à dupliquer leurs unités mémoire dès qu'ils ont atteint la quadruple émission, pour les CPU grand public. La microarchitecture Core d'Intel utilisait la quadruple émission, avec deux unités mémoire et trois ALU entières. Les microarchitectures d'AMD K5 et K6 utilisaient aussi la quadruple émission et avaient deux ALUs pour deux unités mémoire.
Plus récemment, la microarchitecture ''Golden Cove'' dispose de cinq unités de calcul trois unités mémoire (deux spécialisées dans les écritures, trois dans les lectures). La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire.Il y a donc soit égalité, soit avantage pour les ALU entières.
Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Le banc de registre d'un processeur superscalaire===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Une autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage.
===Les unités d'exécution dans le désordre===
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions auto-aligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions auto-aligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez longs, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
ja6prpa1duoewbo54956v5ozj3gt4yb
773173
773172
2026-09-25T20:10:36Z
Mewtow
31375
/* Le partage des ports d'émission */
773173
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===L'impact des contraintes d’appariement sur la performance===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour la suite du chapitre===
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==Le ''front-end'' des processeurs superscalaires==
Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple.
Intuitivement, charger, décoder et 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 certaines processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. 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
|}
Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs.
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===L'émission parallèle des instructions===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elle sont propagée au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage).
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
==Les processeurs implémentés avec plusieurs pipelines==
Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Vous vous dites que l'implémentation d'un processeur superscalaire n'est pas la même suivant que le CPU utilise l'exécution dans le désordre, ou non. Mais dans les faits, la distinction est plus subtile que ca. Pour l'expliquer, je vais utiliser une classification originale, qui sépare les processeurs superscalaires en trois sous-types. La distinction se fait sur la manière dont la superscalarité est implémentée en matériel.La classification que j'utilise distingue :
* Les processeurs avec plusieurs pipelines, sans exécution dans le désordre.
* Les processeurs superscalaires avec un pipeline dynamique.
* Les processeurs avec des fenêtres d'instruction décentralisées.
La distinction n'est pas qu'une question d'implémentation matérielle. Elle impacte aussi le ratio entre instructions chargées/décodées et instructions émises. Vous vous dites que les deux sont égales, mais ce n'est le cas que sur les processeurs avec N pipelines ! Sur les autres processeurs, il est possible de décoder 4 instructions, mais d'en émettre maximum 6 par cycle ! Et sur les CPU avec des fenêtres d'instruction décentralisées, il y a aussi une différence entre le nombre d'instructions qui sont ''dispatch'' et ''schedule''.
La catégorie la plus simple est celle des '''processeurs avec plusieurs pipelines'''. Ce sont des processeurs superscalaires sans exécution dans le désordre. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction.
Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, d'une FPU, éventuellement d'un second banc de registres, guère plus. Il permet d'exécuter une seconde opération de calcul, pas plus, ce qui est à l'origine de contraintes d'appariemment assez fortes. De plus, il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section.
===La double émission entière-flottante===
L'exemple le plus simple utilise un pipeline pour les opérations flottantes, séparé du pipeline originel. Pour bien expliquer les choses, prenons le processeur illustré ci-dessous. Un processeur a deux ''back-end'' séparés, un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par l'ALU entière, passent éventuellement dans l'unité mémoire, pour ensuite arriver à l'étape de ''writeback''. La FPU et le banc de registre flottant ont eux aussi leur propre ''back-end'' séparé.
[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]]
Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaqaue pipeline, à chaque cycle.
L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine.
Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants.
===La double émission entière===
Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait.
Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
==Les processeurs superscalaires dynamiques==
Les processeurs précédents, avec 2 pipelines, avaient une unité d'émission assez simple. Elle avait deux ports de décodage, et deux ports d'émission. Le nombre de ports de décodage et d'émission étaient identiques. Vous vous dites que c'est naturel, et intuitif, que c'est normal pour un processeur superscalaire. Mais en réalité, la majorité des processeurs superscalaires a plus de ports d'émission que de décodage. Expliquer pourquoi va cependant nous demander de parler de comment la superscalarité exploite les unités de calcul.
Posons-nous cette question : de combien d'unités un processeur superscalaire a besoin ? Les unités en question ne sont nécessairement des unité de calcul, l'unité mémoire et l'unité de branchement comptent aussi. La réponse dépend du nombre de voies. Si le ''front-end'' charge et décode un paquet de N instructions, exécuter ces N instructions simultanément demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section.
La réponse dépend aussi des contraintes d’appariement. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission parfaite devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement, ce qui ferait passer de 6 unités à 12. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, le cout en circuit encore plus prohibitif !
En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée, alors que la double émission parait plus adaptée.
Et c'est là que la différence entre ports d'émission et de décodage devient intéressante. L'idée est d'avoir une unité d'émission avec deux ports de décodage, et 4 port d'émission (un par unité). Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux micro-opérations tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu.
Les tout premiers CPU superscalaires étaient nombreux à utiliser cette technique. Les exemples classiques sont l'HyperSPARC, de l'alpha 21064 et du PowerPC 603. C'étaient des CPU double émission, avec une ALU entière, une unité mémoire et une FPU. Nous parlerons de '''partitionnement INT-FLOAT-MEM'''. Les contraintes d’appariement variaient grandement suivant le processeur.
Le PowerPC 603 implémentait une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur.
Avant de poursuivre, précisons que la FPU regroupe un additionneur flottant et un multiplieur flottant. Et ce fait peut être exploité pour approfondir le partitionnement. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions).
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple, facile à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. La raison est que le partitionnement permet d'émettre des paires, triplets, quadruplets de micro-opérations qui sont rares en pratique.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''.
Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, 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. Et enfin, il faut rajouter un port d'émission pour pouvoir émettre une micro-opération entière en plus.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle.
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
Reste à connecter les unités de calcul à l'unité d'émission (indirectement, car il y a des étages de pipeline entre les deux). Une implémentation basique utilisee un port d'émission par unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La présence d'un circuit multiplieur et d'un ''barrel shifter'' pose des problèmes pour le partitionnement. Il est possible d'avoir un port d'émission dédié au multiplieur, et un autre au ''barrel shifter'', qui sont séparés de ceux pour l'ALU entière. Il est alors possible d'émettre une opération entière, une multiplication et un décalage, simultanément. Nous parlerons de '''partitionnement entier''' pour désigner cette possibilité.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
Le partitionnement entier est cependant très rare, car le gain en performance serait trop faible au regard des couts. Quelques processeurs historiques l'utilisaient, mais ce n'est plus le cas de nos jours. La raison est que les multiplications et les décalages sont assez rares dans le code, ce qui fait que le port d'émission associés sont sous-utilisés. Et un port d'émission a un cout en circuit assez important. 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.
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul. L'exemple classique est justement lié aux circuits multiplieur et ''barrel shifters''. L'idée est que le multiplieur et le ''barrel shifter'' sont mis sur le même port d'émission qu'une ALU entière.
Prenons par exemple le processeur à double émission entière illustré ci-contre. Un port d'émission est relié à une ALU entière seule, et l'autre port d'émission alimente la seconde ALU entière et le multiplieur. La conséquence est que le processeur peut émettre deux opérations entières, dont une multiplication. Si on avait utilisé un port d'émission dédié au multiplieur, il aurait été possible d'émettre deux opérations entières et une multiplication. On perd donc une voie, mais on gagne en flexibilité et on économise des circuits.
[[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]]
En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais le partage des ports d'émission peut appliquer à n'importe quelle unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. 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.
La solution est de partager des ports d'émission, pour passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple relie une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
==L'évolution historique de la superscalarité==
Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autre unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents.
* Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU.
* La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants.
* La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables.
* Le '''partage des ports d'émission''' réduit le cout en circuit du CPU, pour des pertes de performances mineures si bien fait.
De nos jours, le partitionnement est utilisé en complément de la duplication des ALU entières, et de la duplication des autres unités. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. Il est intéressant de regarder comment les processeurs superscalaires ont évolué dans le temps.
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. La plupart des CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 sont rapidement montés à de la quadruple émission, avec quelques exceptions. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 ||
|-
! 1995
| 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000
|-
! 1996
| class="f_bleu | 620 || || || || class="f_bleu | 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
===Les processeurs historiques à double émission===
Pour commencer, étudions le cas des premiers CPU superscalaires à double émission. Ils sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Notamment, la duplication des autres unités ne sert pour ainsi dire à rien, ce qui limite les possibilités, mais rendra les explications plus simples.
Il pouvaient se classer en trois types principaux, assez stricts, avec quelques processeurs qui ne rentraient pas dans ces cases. Le premier utilise un ''partitionnement complet'' entre ALU, FPU et unité mémoire. Le second utilisait la double émission entière-flottante. Le troisième utilisait la double émission entière. Voici le tableau chronologique disant quel CPU utilisait quelle technique.
* Le partitionnement INT-FLOAT-MEM est en jaune.
* La double émission entière-flottante est en vert.
* La double émission entière est en rouge.
* Les CPU barrés utilisent la quadruple émission et seront vu dans la section suivante.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| class="f_vert | RS/6000 || || || class="f_rouge | i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| class="f_vert | RSC || class="f_vert | PA 7100 || SuperSPARC || || class="f_jaune | 21064 ||
|-
! 1993
| class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || ||
|-
! 1994
| <s>POWER 2</s> || PA 7200 || || || <s>21164</s> ||
|}
Vous voyez que certains processeur sont laissés de côté dans le tableau précédent. C'est car ils ne rentrent pas dans les catégories précédentes et faisaient autrement. Si on omet le Motorola 88110, qui utilisait une forme de double émission assez avancée, les deux CPUs qui vont nous intéresser sont le PA-RISC PA 7200 d'HP, et le SuperSPARC.
Le PA 7200, sorti en 1994, est un PA-7100 auquel on aurait rajouté une ALU entière. Précisons que ce processeur exécutait les multiplications entières dans la FPU, comme son prédécesseur. Les combinaisons autorisées étaient donc INT + INT, INT + MEM, INT + FLOAT/MUL, MEM + FLOAT/MUL. Les combinaisons autorisées par le partitionnement INT-FLOAT-MEM étaient donc possibles, en plus des combinaisons INT + INT et INT + INT + MUL. Autrement dit, il pouvait faire toute ce que permettait un CPU à double émission entière, plus les combinaisons FLOAT/MUL + MEM.
Le SuperSPARC était un CPU triple émission, avec deux ALU entières, une FPU et une unité mémoire. Il pouvait émettre, à chaque cycle, trois µops en piochant dans deux opérations entières, un accès mémoire, une opération flottante. Le SuperSPARC avait de nombreuses contraintes d’appariement. Par exemple, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Une bonne partie de ces contraintes était lié au banc de registre, qui n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions.
: La seconde ALU a été abandonnée dans l'HyperSPARC, qui est de plus repassé en double émission, ce qui est une évolution à rebours de l'évolution des autres CPUs. Le SuperSPARC était en avance sur son temps, ce quelques années, alors que l'HyperSPARC a un design de son temps.
===Les processeurs historiques à quadruple émission===
C'est la même année , en 1994, que la quadruple émission est arrivée et que les CPU ont commencé à avoir de plus en plus d'unités de calcul. Même les designs restés en double émission avaient ajouté des unités de calcul. Il y avait alors trois stratégies pour la quadruple émission.
La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Les FPUs étaient soit dupliquées, soit scindées en deux en séparant l'additionneur du multiplieur flottant. Le POWER 2 dupliquait ses FPUs à la perfection, alors que l'21264 partitionnait la FPU. Les accès mémoire étaient traités comme des opérations entières, et le processeur pouvait : soit en faire un seul par cycle, soit en émettre deux, sous conditions (le cache devait être double port pour lire/écrire deux données à la fois, comme on le verra dans la suite du chapitre).
Les processeurs MIPS R8000 et R10000 allaient un peu plus loin, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités.
La seconde implémentation dupliquait l'ALU entière et combinait le tout avec un partitionnement INT-FLOAT-MEM. De quoi exécuter en même temps deux opérations entières, une opération flottante, et un accès mémoire. 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).
Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, avec au minimum deux ALU entières. Pour comprendre ce que je veux dire, prenons l'exemple de l'UltraSPARC. Il avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplier flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes. Le processeur pouvait émettre 4 instructions, tant qu'elles atterrissaient dans des unités différentes.
La même méthode peut être utilisée pour la double émission. Le Motorola 88110, sorti en 1991, était un CPU à double émission avec pas moins de 10 unités : deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise.
La morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes. Le tout est résumé dans le tableau ci-dessous, avec :
* en vert les CPU utilisant la première stratégie ;
* en rouge les CPU utilisant la seconde stratégie ;
* en jaune les CPU utilisant la troisième stratégie.
{|class="wikitable"
|+ Processeurs quadruple émission historiques
|-
! Année !! Processeur !! Unités
|-
! 1991
| class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques
|-
|-
! rowspan="2" | 1994
| class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'')
|-
| class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM
|-
! rowspan="3" | 1995
| class="f_jaune | PA 8000 || class="f_jaune | 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD
|-
| class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques
|-
| MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM
|-
! rowspan="2" | 1996
| class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM
|-
| class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL)
|}
===Les processeurs superscalaires modernes===
Les CPU superscalaires modernes ont des design légèrement différents des CPU historiques. Ils dupliquent l'unité entière, mais leur partitionnement est différent. En général, les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Par exemple, un CPU à 6 voies pourra émettre/exécuter 6 opérations entières simultanément, grâce à 6 ALU entières. Mais il s'agit là d'un maximum et certaines peuvent être remplacées par une opération flottante ou mémoire, qui utilisent l'unité mémoire et/ou la FPU.
Un exemple assez parlant est les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon. C'était une microarchitecture à triple émission, avec trois ALU entières et trois unités mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements.
Il s'agit là d'une tendance, cependant. Les CPU superscalaires tendent à mettre le paquet niveau opérations arithmétiques, mais pas forcément au point de remplir la totalité de la largeur du processeur. La règle est que pour N voies, le processeur dispose d'au moins N/2 ALU entières. Attention, cela ne signifie pas que le processeur émet systématiquement N/2 opération entières. Il peut par exemple émettre une combinaison d'opérations flottantes et mémoire, avec des branchements. Et N/2 est une borne basse, la plupart des CPU x86 ont environ tournent plutôt autour des 2/3, voire des 3/4.
En général, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. La conséquence est que les unités mémoire sont dupliquées, en seconde place dans l'ordre d'importance. Les processeurs x86 ont commencé à dupliquer leurs unités mémoire dès qu'ils ont atteint la quadruple émission, pour les CPU grand public. La microarchitecture Core d'Intel utilisait la quadruple émission, avec deux unités mémoire et trois ALU entières. Les microarchitectures d'AMD K5 et K6 utilisaient aussi la quadruple émission et avaient deux ALUs pour deux unités mémoire.
Plus récemment, la microarchitecture ''Golden Cove'' dispose de cinq unités de calcul trois unités mémoire (deux spécialisées dans les écritures, trois dans les lectures). La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire.Il y a donc soit égalité, soit avantage pour les ALU entières.
Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Le banc de registre d'un processeur superscalaire===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Une autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage.
===Les unités d'exécution dans le désordre===
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions auto-aligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions auto-aligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez longs, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
h42wacl1g2mbe2q1tbiliijk6w4v5bd
773174
773173
2026-09-25T20:11:19Z
Mewtow
31375
/* L'évolution historique de la superscalarité */
773174
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===L'impact des contraintes d’appariement sur la performance===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour la suite du chapitre===
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==Le ''front-end'' des processeurs superscalaires==
Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple.
Intuitivement, charger, décoder et 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 certaines processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. 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
|}
Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs.
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===L'émission parallèle des instructions===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elle sont propagée au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage).
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
==Les processeurs implémentés avec plusieurs pipelines==
Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Vous vous dites que l'implémentation d'un processeur superscalaire n'est pas la même suivant que le CPU utilise l'exécution dans le désordre, ou non. Mais dans les faits, la distinction est plus subtile que ca. Pour l'expliquer, je vais utiliser une classification originale, qui sépare les processeurs superscalaires en trois sous-types. La distinction se fait sur la manière dont la superscalarité est implémentée en matériel.La classification que j'utilise distingue :
* Les processeurs avec plusieurs pipelines, sans exécution dans le désordre.
* Les processeurs superscalaires avec un pipeline dynamique.
* Les processeurs avec des fenêtres d'instruction décentralisées.
La distinction n'est pas qu'une question d'implémentation matérielle. Elle impacte aussi le ratio entre instructions chargées/décodées et instructions émises. Vous vous dites que les deux sont égales, mais ce n'est le cas que sur les processeurs avec N pipelines ! Sur les autres processeurs, il est possible de décoder 4 instructions, mais d'en émettre maximum 6 par cycle ! Et sur les CPU avec des fenêtres d'instruction décentralisées, il y a aussi une différence entre le nombre d'instructions qui sont ''dispatch'' et ''schedule''.
La catégorie la plus simple est celle des '''processeurs avec plusieurs pipelines'''. Ce sont des processeurs superscalaires sans exécution dans le désordre. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction.
Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, d'une FPU, éventuellement d'un second banc de registres, guère plus. Il permet d'exécuter une seconde opération de calcul, pas plus, ce qui est à l'origine de contraintes d'appariemment assez fortes. De plus, il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section.
===La double émission entière-flottante===
L'exemple le plus simple utilise un pipeline pour les opérations flottantes, séparé du pipeline originel. Pour bien expliquer les choses, prenons le processeur illustré ci-dessous. Un processeur a deux ''back-end'' séparés, un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par l'ALU entière, passent éventuellement dans l'unité mémoire, pour ensuite arriver à l'étape de ''writeback''. La FPU et le banc de registre flottant ont eux aussi leur propre ''back-end'' séparé.
[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]]
Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaqaue pipeline, à chaque cycle.
L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine.
Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants.
===La double émission entière===
Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait.
Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
==Les processeurs superscalaires dynamiques==
Les processeurs précédents, avec 2 pipelines, avaient une unité d'émission assez simple. Elle avait deux ports de décodage, et deux ports d'émission. Le nombre de ports de décodage et d'émission étaient identiques. Vous vous dites que c'est naturel, et intuitif, que c'est normal pour un processeur superscalaire. Mais en réalité, la majorité des processeurs superscalaires a plus de ports d'émission que de décodage. Expliquer pourquoi va cependant nous demander de parler de comment la superscalarité exploite les unités de calcul.
Posons-nous cette question : de combien d'unités un processeur superscalaire a besoin ? Les unités en question ne sont nécessairement des unité de calcul, l'unité mémoire et l'unité de branchement comptent aussi. La réponse dépend du nombre de voies. Si le ''front-end'' charge et décode un paquet de N instructions, exécuter ces N instructions simultanément demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section.
La réponse dépend aussi des contraintes d’appariement. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission parfaite devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement, ce qui ferait passer de 6 unités à 12. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, le cout en circuit encore plus prohibitif !
En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée, alors que la double émission parait plus adaptée.
Et c'est là que la différence entre ports d'émission et de décodage devient intéressante. L'idée est d'avoir une unité d'émission avec deux ports de décodage, et 4 port d'émission (un par unité). Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux micro-opérations tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu.
Les tout premiers CPU superscalaires étaient nombreux à utiliser cette technique. Les exemples classiques sont l'HyperSPARC, de l'alpha 21064 et du PowerPC 603. C'étaient des CPU double émission, avec une ALU entière, une unité mémoire et une FPU. Nous parlerons de '''partitionnement INT-FLOAT-MEM'''. Les contraintes d’appariement variaient grandement suivant le processeur.
Le PowerPC 603 implémentait une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur.
Avant de poursuivre, précisons que la FPU regroupe un additionneur flottant et un multiplieur flottant. Et ce fait peut être exploité pour approfondir le partitionnement. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions).
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple, facile à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. La raison est que le partitionnement permet d'émettre des paires, triplets, quadruplets de micro-opérations qui sont rares en pratique.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''.
Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, 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. Et enfin, il faut rajouter un port d'émission pour pouvoir émettre une micro-opération entière en plus.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle.
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
Reste à connecter les unités de calcul à l'unité d'émission (indirectement, car il y a des étages de pipeline entre les deux). Une implémentation basique utilisee un port d'émission par unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La présence d'un circuit multiplieur et d'un ''barrel shifter'' pose des problèmes pour le partitionnement. Il est possible d'avoir un port d'émission dédié au multiplieur, et un autre au ''barrel shifter'', qui sont séparés de ceux pour l'ALU entière. Il est alors possible d'émettre une opération entière, une multiplication et un décalage, simultanément. Nous parlerons de '''partitionnement entier''' pour désigner cette possibilité.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
Le partitionnement entier est cependant très rare, car le gain en performance serait trop faible au regard des couts. Quelques processeurs historiques l'utilisaient, mais ce n'est plus le cas de nos jours. La raison est que les multiplications et les décalages sont assez rares dans le code, ce qui fait que le port d'émission associés sont sous-utilisés. Et un port d'émission a un cout en circuit assez important. 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.
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul. L'exemple classique est justement lié aux circuits multiplieur et ''barrel shifters''. L'idée est que le multiplieur et le ''barrel shifter'' sont mis sur le même port d'émission qu'une ALU entière.
Prenons par exemple le processeur à double émission entière illustré ci-contre. Un port d'émission est relié à une ALU entière seule, et l'autre port d'émission alimente la seconde ALU entière et le multiplieur. La conséquence est que le processeur peut émettre deux opérations entières, dont une multiplication. Si on avait utilisé un port d'émission dédié au multiplieur, il aurait été possible d'émettre deux opérations entières et une multiplication. On perd donc une voie, mais on gagne en flexibilité et on économise des circuits.
[[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]]
En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais le partage des ports d'émission peut appliquer à n'importe quelle unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. 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.
La solution est de partager des ports d'émission, pour passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple relie une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPus superscalaires mélangent les trois techniques===
Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autre unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents.
* Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU.
* La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants.
* La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables.
* Le '''partage des ports d'émission''' réduit le cout en circuit du CPU, pour des pertes de performances mineures si bien fait.
De nos jours, le partitionnement est utilisé en complément de la duplication des ALU entières, et de la duplication des autres unités. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. Il est intéressant de regarder comment les processeurs superscalaires ont évolué dans le temps.
Prenons l'exemple des processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
==L'évolution historique de la superscalarité==
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. La plupart des CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 sont rapidement montés à de la quadruple émission, avec quelques exceptions. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 ||
|-
! 1995
| 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000
|-
! 1996
| class="f_bleu | 620 || || || || class="f_bleu | 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
===Les processeurs historiques à double émission===
Pour commencer, étudions le cas des premiers CPU superscalaires à double émission. Ils sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Notamment, la duplication des autres unités ne sert pour ainsi dire à rien, ce qui limite les possibilités, mais rendra les explications plus simples.
Il pouvaient se classer en trois types principaux, assez stricts, avec quelques processeurs qui ne rentraient pas dans ces cases. Le premier utilise un ''partitionnement complet'' entre ALU, FPU et unité mémoire. Le second utilisait la double émission entière-flottante. Le troisième utilisait la double émission entière. Voici le tableau chronologique disant quel CPU utilisait quelle technique.
* Le partitionnement INT-FLOAT-MEM est en jaune.
* La double émission entière-flottante est en vert.
* La double émission entière est en rouge.
* Les CPU barrés utilisent la quadruple émission et seront vu dans la section suivante.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| class="f_vert | RS/6000 || || || class="f_rouge | i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| class="f_vert | RSC || class="f_vert | PA 7100 || SuperSPARC || || class="f_jaune | 21064 ||
|-
! 1993
| class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || ||
|-
! 1994
| <s>POWER 2</s> || PA 7200 || || || <s>21164</s> ||
|}
Vous voyez que certains processeur sont laissés de côté dans le tableau précédent. C'est car ils ne rentrent pas dans les catégories précédentes et faisaient autrement. Si on omet le Motorola 88110, qui utilisait une forme de double émission assez avancée, les deux CPUs qui vont nous intéresser sont le PA-RISC PA 7200 d'HP, et le SuperSPARC.
Le PA 7200, sorti en 1994, est un PA-7100 auquel on aurait rajouté une ALU entière. Précisons que ce processeur exécutait les multiplications entières dans la FPU, comme son prédécesseur. Les combinaisons autorisées étaient donc INT + INT, INT + MEM, INT + FLOAT/MUL, MEM + FLOAT/MUL. Les combinaisons autorisées par le partitionnement INT-FLOAT-MEM étaient donc possibles, en plus des combinaisons INT + INT et INT + INT + MUL. Autrement dit, il pouvait faire toute ce que permettait un CPU à double émission entière, plus les combinaisons FLOAT/MUL + MEM.
Le SuperSPARC était un CPU triple émission, avec deux ALU entières, une FPU et une unité mémoire. Il pouvait émettre, à chaque cycle, trois µops en piochant dans deux opérations entières, un accès mémoire, une opération flottante. Le SuperSPARC avait de nombreuses contraintes d’appariement. Par exemple, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Une bonne partie de ces contraintes était lié au banc de registre, qui n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions.
: La seconde ALU a été abandonnée dans l'HyperSPARC, qui est de plus repassé en double émission, ce qui est une évolution à rebours de l'évolution des autres CPUs. Le SuperSPARC était en avance sur son temps, ce quelques années, alors que l'HyperSPARC a un design de son temps.
===Les processeurs historiques à quadruple émission===
C'est la même année , en 1994, que la quadruple émission est arrivée et que les CPU ont commencé à avoir de plus en plus d'unités de calcul. Même les designs restés en double émission avaient ajouté des unités de calcul. Il y avait alors trois stratégies pour la quadruple émission.
La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Les FPUs étaient soit dupliquées, soit scindées en deux en séparant l'additionneur du multiplieur flottant. Le POWER 2 dupliquait ses FPUs à la perfection, alors que l'21264 partitionnait la FPU. Les accès mémoire étaient traités comme des opérations entières, et le processeur pouvait : soit en faire un seul par cycle, soit en émettre deux, sous conditions (le cache devait être double port pour lire/écrire deux données à la fois, comme on le verra dans la suite du chapitre).
Les processeurs MIPS R8000 et R10000 allaient un peu plus loin, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités.
La seconde implémentation dupliquait l'ALU entière et combinait le tout avec un partitionnement INT-FLOAT-MEM. De quoi exécuter en même temps deux opérations entières, une opération flottante, et un accès mémoire. 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).
Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, avec au minimum deux ALU entières. Pour comprendre ce que je veux dire, prenons l'exemple de l'UltraSPARC. Il avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplier flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes. Le processeur pouvait émettre 4 instructions, tant qu'elles atterrissaient dans des unités différentes.
La même méthode peut être utilisée pour la double émission. Le Motorola 88110, sorti en 1991, était un CPU à double émission avec pas moins de 10 unités : deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise.
La morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes. Le tout est résumé dans le tableau ci-dessous, avec :
* en vert les CPU utilisant la première stratégie ;
* en rouge les CPU utilisant la seconde stratégie ;
* en jaune les CPU utilisant la troisième stratégie.
{|class="wikitable"
|+ Processeurs quadruple émission historiques
|-
! Année !! Processeur !! Unités
|-
! 1991
| class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques
|-
|-
! rowspan="2" | 1994
| class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'')
|-
| class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM
|-
! rowspan="3" | 1995
| class="f_jaune | PA 8000 || class="f_jaune | 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD
|-
| class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques
|-
| MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM
|-
! rowspan="2" | 1996
| class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM
|-
| class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL)
|}
===Les processeurs superscalaires modernes===
Les CPU superscalaires modernes ont des design légèrement différents des CPU historiques. Ils dupliquent l'unité entière, mais leur partitionnement est différent. En général, les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Par exemple, un CPU à 6 voies pourra émettre/exécuter 6 opérations entières simultanément, grâce à 6 ALU entières. Mais il s'agit là d'un maximum et certaines peuvent être remplacées par une opération flottante ou mémoire, qui utilisent l'unité mémoire et/ou la FPU.
Un exemple assez parlant est les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon. C'était une microarchitecture à triple émission, avec trois ALU entières et trois unités mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements.
Il s'agit là d'une tendance, cependant. Les CPU superscalaires tendent à mettre le paquet niveau opérations arithmétiques, mais pas forcément au point de remplir la totalité de la largeur du processeur. La règle est que pour N voies, le processeur dispose d'au moins N/2 ALU entières. Attention, cela ne signifie pas que le processeur émet systématiquement N/2 opération entières. Il peut par exemple émettre une combinaison d'opérations flottantes et mémoire, avec des branchements. Et N/2 est une borne basse, la plupart des CPU x86 ont environ tournent plutôt autour des 2/3, voire des 3/4.
En général, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. La conséquence est que les unités mémoire sont dupliquées, en seconde place dans l'ordre d'importance. Les processeurs x86 ont commencé à dupliquer leurs unités mémoire dès qu'ils ont atteint la quadruple émission, pour les CPU grand public. La microarchitecture Core d'Intel utilisait la quadruple émission, avec deux unités mémoire et trois ALU entières. Les microarchitectures d'AMD K5 et K6 utilisaient aussi la quadruple émission et avaient deux ALUs pour deux unités mémoire.
Plus récemment, la microarchitecture ''Golden Cove'' dispose de cinq unités de calcul trois unités mémoire (deux spécialisées dans les écritures, trois dans les lectures). La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire.Il y a donc soit égalité, soit avantage pour les ALU entières.
Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Le banc de registre d'un processeur superscalaire===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Une autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage.
===Les unités d'exécution dans le désordre===
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions auto-aligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions auto-aligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez longs, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
602ifmyvh8g40bdj2ixlsy33dk9oy4t
773175
773174
2026-09-25T20:12:25Z
Mewtow
31375
/* Les CPus superscalaires mélangent les trois techniques */
773175
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===L'impact des contraintes d’appariement sur la performance===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour la suite du chapitre===
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==Le ''front-end'' des processeurs superscalaires==
Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple.
Intuitivement, charger, décoder et 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 certaines processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. 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
|}
Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs.
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===L'émission parallèle des instructions===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elle sont propagée au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage).
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
==Les processeurs implémentés avec plusieurs pipelines==
Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Vous vous dites que l'implémentation d'un processeur superscalaire n'est pas la même suivant que le CPU utilise l'exécution dans le désordre, ou non. Mais dans les faits, la distinction est plus subtile que ca. Pour l'expliquer, je vais utiliser une classification originale, qui sépare les processeurs superscalaires en trois sous-types. La distinction se fait sur la manière dont la superscalarité est implémentée en matériel.La classification que j'utilise distingue :
* Les processeurs avec plusieurs pipelines, sans exécution dans le désordre.
* Les processeurs superscalaires avec un pipeline dynamique.
* Les processeurs avec des fenêtres d'instruction décentralisées.
La distinction n'est pas qu'une question d'implémentation matérielle. Elle impacte aussi le ratio entre instructions chargées/décodées et instructions émises. Vous vous dites que les deux sont égales, mais ce n'est le cas que sur les processeurs avec N pipelines ! Sur les autres processeurs, il est possible de décoder 4 instructions, mais d'en émettre maximum 6 par cycle ! Et sur les CPU avec des fenêtres d'instruction décentralisées, il y a aussi une différence entre le nombre d'instructions qui sont ''dispatch'' et ''schedule''.
La catégorie la plus simple est celle des '''processeurs avec plusieurs pipelines'''. Ce sont des processeurs superscalaires sans exécution dans le désordre. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction.
Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, d'une FPU, éventuellement d'un second banc de registres, guère plus. Il permet d'exécuter une seconde opération de calcul, pas plus, ce qui est à l'origine de contraintes d'appariemment assez fortes. De plus, il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section.
===La double émission entière-flottante===
L'exemple le plus simple utilise un pipeline pour les opérations flottantes, séparé du pipeline originel. Pour bien expliquer les choses, prenons le processeur illustré ci-dessous. Un processeur a deux ''back-end'' séparés, un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par l'ALU entière, passent éventuellement dans l'unité mémoire, pour ensuite arriver à l'étape de ''writeback''. La FPU et le banc de registre flottant ont eux aussi leur propre ''back-end'' séparé.
[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]]
Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaqaue pipeline, à chaque cycle.
L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine.
Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants.
===La double émission entière===
Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait.
Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
==Les processeurs superscalaires dynamiques==
Les processeurs précédents, avec 2 pipelines, avaient une unité d'émission assez simple. Elle avait deux ports de décodage, et deux ports d'émission. Le nombre de ports de décodage et d'émission étaient identiques. Vous vous dites que c'est naturel, et intuitif, que c'est normal pour un processeur superscalaire. Mais en réalité, la majorité des processeurs superscalaires a plus de ports d'émission que de décodage. Expliquer pourquoi va cependant nous demander de parler de comment la superscalarité exploite les unités de calcul.
Posons-nous cette question : de combien d'unités un processeur superscalaire a besoin ? Les unités en question ne sont nécessairement des unité de calcul, l'unité mémoire et l'unité de branchement comptent aussi. La réponse dépend du nombre de voies. Si le ''front-end'' charge et décode un paquet de N instructions, exécuter ces N instructions simultanément demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section.
La réponse dépend aussi des contraintes d’appariement. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission parfaite devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement, ce qui ferait passer de 6 unités à 12. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, le cout en circuit encore plus prohibitif !
En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée, alors que la double émission parait plus adaptée.
Et c'est là que la différence entre ports d'émission et de décodage devient intéressante. L'idée est d'avoir une unité d'émission avec deux ports de décodage, et 4 port d'émission (un par unité). Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux micro-opérations tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu.
Les tout premiers CPU superscalaires étaient nombreux à utiliser cette technique. Les exemples classiques sont l'HyperSPARC, de l'alpha 21064 et du PowerPC 603. C'étaient des CPU double émission, avec une ALU entière, une unité mémoire et une FPU. Nous parlerons de '''partitionnement INT-FLOAT-MEM'''. Les contraintes d’appariement variaient grandement suivant le processeur.
Le PowerPC 603 implémentait une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur.
Avant de poursuivre, précisons que la FPU regroupe un additionneur flottant et un multiplieur flottant. Et ce fait peut être exploité pour approfondir le partitionnement. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions).
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple, facile à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. La raison est que le partitionnement permet d'émettre des paires, triplets, quadruplets de micro-opérations qui sont rares en pratique.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''.
Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, 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. Et enfin, il faut rajouter un port d'émission pour pouvoir émettre une micro-opération entière en plus.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle.
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
Reste à connecter les unités de calcul à l'unité d'émission (indirectement, car il y a des étages de pipeline entre les deux). Une implémentation basique utilisee un port d'émission par unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La présence d'un circuit multiplieur et d'un ''barrel shifter'' pose des problèmes pour le partitionnement. Il est possible d'avoir un port d'émission dédié au multiplieur, et un autre au ''barrel shifter'', qui sont séparés de ceux pour l'ALU entière. Il est alors possible d'émettre une opération entière, une multiplication et un décalage, simultanément. Nous parlerons de '''partitionnement entier''' pour désigner cette possibilité.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
Le partitionnement entier est cependant très rare, car le gain en performance serait trop faible au regard des couts. Quelques processeurs historiques l'utilisaient, mais ce n'est plus le cas de nos jours. La raison est que les multiplications et les décalages sont assez rares dans le code, ce qui fait que le port d'émission associés sont sous-utilisés. Et un port d'émission a un cout en circuit assez important. 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.
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul. L'exemple classique est justement lié aux circuits multiplieur et ''barrel shifters''. L'idée est que le multiplieur et le ''barrel shifter'' sont mis sur le même port d'émission qu'une ALU entière.
Prenons par exemple le processeur à double émission entière illustré ci-contre. Un port d'émission est relié à une ALU entière seule, et l'autre port d'émission alimente la seconde ALU entière et le multiplieur. La conséquence est que le processeur peut émettre deux opérations entières, dont une multiplication. Si on avait utilisé un port d'émission dédié au multiplieur, il aurait été possible d'émettre deux opérations entières et une multiplication. On perd donc une voie, mais on gagne en flexibilité et on économise des circuits.
[[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]]
En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais le partage des ports d'émission peut appliquer à n'importe quelle unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. 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.
La solution est de partager des ports d'émission, pour passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple relie une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires mélangent les trois techniques===
Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autre unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents.
* Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU.
* La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants.
* La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables.
* Le '''partage des ports d'émission''' réduit le cout en circuit du CPU, pour des pertes de performances mineures si bien fait.
De nos jours, le partitionnement est utilisé en complément de la duplication des ALU entières, de la duplication des autres unités, et du partage des ports d'émission. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. Le partage des ports d'émission a lui aussi mis un peu de temps pour s'imposer.
Prenons l'exemple des processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
==L'évolution historique de la superscalarité==
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. La plupart des CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 sont rapidement montés à de la quadruple émission, avec quelques exceptions. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 ||
|-
! 1995
| 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000
|-
! 1996
| class="f_bleu | 620 || || || || class="f_bleu | 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
===Les processeurs historiques à double émission===
Pour commencer, étudions le cas des premiers CPU superscalaires à double émission. Ils sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Notamment, la duplication des autres unités ne sert pour ainsi dire à rien, ce qui limite les possibilités, mais rendra les explications plus simples.
Il pouvaient se classer en trois types principaux, assez stricts, avec quelques processeurs qui ne rentraient pas dans ces cases. Le premier utilise un ''partitionnement complet'' entre ALU, FPU et unité mémoire. Le second utilisait la double émission entière-flottante. Le troisième utilisait la double émission entière. Voici le tableau chronologique disant quel CPU utilisait quelle technique.
* Le partitionnement INT-FLOAT-MEM est en jaune.
* La double émission entière-flottante est en vert.
* La double émission entière est en rouge.
* Les CPU barrés utilisent la quadruple émission et seront vu dans la section suivante.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| class="f_vert | RS/6000 || || || class="f_rouge | i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| class="f_vert | RSC || class="f_vert | PA 7100 || SuperSPARC || || class="f_jaune | 21064 ||
|-
! 1993
| class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || ||
|-
! 1994
| <s>POWER 2</s> || PA 7200 || || || <s>21164</s> ||
|}
Vous voyez que certains processeur sont laissés de côté dans le tableau précédent. C'est car ils ne rentrent pas dans les catégories précédentes et faisaient autrement. Si on omet le Motorola 88110, qui utilisait une forme de double émission assez avancée, les deux CPUs qui vont nous intéresser sont le PA-RISC PA 7200 d'HP, et le SuperSPARC.
Le PA 7200, sorti en 1994, est un PA-7100 auquel on aurait rajouté une ALU entière. Précisons que ce processeur exécutait les multiplications entières dans la FPU, comme son prédécesseur. Les combinaisons autorisées étaient donc INT + INT, INT + MEM, INT + FLOAT/MUL, MEM + FLOAT/MUL. Les combinaisons autorisées par le partitionnement INT-FLOAT-MEM étaient donc possibles, en plus des combinaisons INT + INT et INT + INT + MUL. Autrement dit, il pouvait faire toute ce que permettait un CPU à double émission entière, plus les combinaisons FLOAT/MUL + MEM.
Le SuperSPARC était un CPU triple émission, avec deux ALU entières, une FPU et une unité mémoire. Il pouvait émettre, à chaque cycle, trois µops en piochant dans deux opérations entières, un accès mémoire, une opération flottante. Le SuperSPARC avait de nombreuses contraintes d’appariement. Par exemple, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Une bonne partie de ces contraintes était lié au banc de registre, qui n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions.
: La seconde ALU a été abandonnée dans l'HyperSPARC, qui est de plus repassé en double émission, ce qui est une évolution à rebours de l'évolution des autres CPUs. Le SuperSPARC était en avance sur son temps, ce quelques années, alors que l'HyperSPARC a un design de son temps.
===Les processeurs historiques à quadruple émission===
C'est la même année , en 1994, que la quadruple émission est arrivée et que les CPU ont commencé à avoir de plus en plus d'unités de calcul. Même les designs restés en double émission avaient ajouté des unités de calcul. Il y avait alors trois stratégies pour la quadruple émission.
La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Les FPUs étaient soit dupliquées, soit scindées en deux en séparant l'additionneur du multiplieur flottant. Le POWER 2 dupliquait ses FPUs à la perfection, alors que l'21264 partitionnait la FPU. Les accès mémoire étaient traités comme des opérations entières, et le processeur pouvait : soit en faire un seul par cycle, soit en émettre deux, sous conditions (le cache devait être double port pour lire/écrire deux données à la fois, comme on le verra dans la suite du chapitre).
Les processeurs MIPS R8000 et R10000 allaient un peu plus loin, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités.
La seconde implémentation dupliquait l'ALU entière et combinait le tout avec un partitionnement INT-FLOAT-MEM. De quoi exécuter en même temps deux opérations entières, une opération flottante, et un accès mémoire. 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).
Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, avec au minimum deux ALU entières. Pour comprendre ce que je veux dire, prenons l'exemple de l'UltraSPARC. Il avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplier flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes. Le processeur pouvait émettre 4 instructions, tant qu'elles atterrissaient dans des unités différentes.
La même méthode peut être utilisée pour la double émission. Le Motorola 88110, sorti en 1991, était un CPU à double émission avec pas moins de 10 unités : deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise.
La morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes. Le tout est résumé dans le tableau ci-dessous, avec :
* en vert les CPU utilisant la première stratégie ;
* en rouge les CPU utilisant la seconde stratégie ;
* en jaune les CPU utilisant la troisième stratégie.
{|class="wikitable"
|+ Processeurs quadruple émission historiques
|-
! Année !! Processeur !! Unités
|-
! 1991
| class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques
|-
|-
! rowspan="2" | 1994
| class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'')
|-
| class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM
|-
! rowspan="3" | 1995
| class="f_jaune | PA 8000 || class="f_jaune | 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD
|-
| class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques
|-
| MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM
|-
! rowspan="2" | 1996
| class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM
|-
| class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL)
|}
===Les processeurs superscalaires modernes===
Les CPU superscalaires modernes ont des design légèrement différents des CPU historiques. Ils dupliquent l'unité entière, mais leur partitionnement est différent. En général, les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Par exemple, un CPU à 6 voies pourra émettre/exécuter 6 opérations entières simultanément, grâce à 6 ALU entières. Mais il s'agit là d'un maximum et certaines peuvent être remplacées par une opération flottante ou mémoire, qui utilisent l'unité mémoire et/ou la FPU.
Un exemple assez parlant est les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon. C'était une microarchitecture à triple émission, avec trois ALU entières et trois unités mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements.
Il s'agit là d'une tendance, cependant. Les CPU superscalaires tendent à mettre le paquet niveau opérations arithmétiques, mais pas forcément au point de remplir la totalité de la largeur du processeur. La règle est que pour N voies, le processeur dispose d'au moins N/2 ALU entières. Attention, cela ne signifie pas que le processeur émet systématiquement N/2 opération entières. Il peut par exemple émettre une combinaison d'opérations flottantes et mémoire, avec des branchements. Et N/2 est une borne basse, la plupart des CPU x86 ont environ tournent plutôt autour des 2/3, voire des 3/4.
En général, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. La conséquence est que les unités mémoire sont dupliquées, en seconde place dans l'ordre d'importance. Les processeurs x86 ont commencé à dupliquer leurs unités mémoire dès qu'ils ont atteint la quadruple émission, pour les CPU grand public. La microarchitecture Core d'Intel utilisait la quadruple émission, avec deux unités mémoire et trois ALU entières. Les microarchitectures d'AMD K5 et K6 utilisaient aussi la quadruple émission et avaient deux ALUs pour deux unités mémoire.
Plus récemment, la microarchitecture ''Golden Cove'' dispose de cinq unités de calcul trois unités mémoire (deux spécialisées dans les écritures, trois dans les lectures). La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire.Il y a donc soit égalité, soit avantage pour les ALU entières.
Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Le banc de registre d'un processeur superscalaire===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Une autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage.
===Les unités d'exécution dans le désordre===
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions auto-aligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions auto-aligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez longs, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
h7t8pj3t7bn3l0lihp9o4wstfhljz1f
773176
773175
2026-09-25T20:15:52Z
Mewtow
31375
/* Les processeurs historiques à double émission */
773176
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===L'impact des contraintes d’appariement sur la performance===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour la suite du chapitre===
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==Le ''front-end'' des processeurs superscalaires==
Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple.
Intuitivement, charger, décoder et 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 certaines processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. 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
|}
Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs.
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===L'émission parallèle des instructions===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elle sont propagée au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage).
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
==Les processeurs implémentés avec plusieurs pipelines==
Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Vous vous dites que l'implémentation d'un processeur superscalaire n'est pas la même suivant que le CPU utilise l'exécution dans le désordre, ou non. Mais dans les faits, la distinction est plus subtile que ca. Pour l'expliquer, je vais utiliser une classification originale, qui sépare les processeurs superscalaires en trois sous-types. La distinction se fait sur la manière dont la superscalarité est implémentée en matériel.La classification que j'utilise distingue :
* Les processeurs avec plusieurs pipelines, sans exécution dans le désordre.
* Les processeurs superscalaires avec un pipeline dynamique.
* Les processeurs avec des fenêtres d'instruction décentralisées.
La distinction n'est pas qu'une question d'implémentation matérielle. Elle impacte aussi le ratio entre instructions chargées/décodées et instructions émises. Vous vous dites que les deux sont égales, mais ce n'est le cas que sur les processeurs avec N pipelines ! Sur les autres processeurs, il est possible de décoder 4 instructions, mais d'en émettre maximum 6 par cycle ! Et sur les CPU avec des fenêtres d'instruction décentralisées, il y a aussi une différence entre le nombre d'instructions qui sont ''dispatch'' et ''schedule''.
La catégorie la plus simple est celle des '''processeurs avec plusieurs pipelines'''. Ce sont des processeurs superscalaires sans exécution dans le désordre. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction.
Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, d'une FPU, éventuellement d'un second banc de registres, guère plus. Il permet d'exécuter une seconde opération de calcul, pas plus, ce qui est à l'origine de contraintes d'appariemment assez fortes. De plus, il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section.
===La double émission entière-flottante===
L'exemple le plus simple utilise un pipeline pour les opérations flottantes, séparé du pipeline originel. Pour bien expliquer les choses, prenons le processeur illustré ci-dessous. Un processeur a deux ''back-end'' séparés, un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par l'ALU entière, passent éventuellement dans l'unité mémoire, pour ensuite arriver à l'étape de ''writeback''. La FPU et le banc de registre flottant ont eux aussi leur propre ''back-end'' séparé.
[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]]
Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaqaue pipeline, à chaque cycle.
L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine.
Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants.
===La double émission entière===
Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait.
Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
==Les processeurs superscalaires dynamiques==
Les processeurs précédents, avec 2 pipelines, avaient une unité d'émission assez simple. Elle avait deux ports de décodage, et deux ports d'émission. Le nombre de ports de décodage et d'émission étaient identiques. Vous vous dites que c'est naturel, et intuitif, que c'est normal pour un processeur superscalaire. Mais en réalité, la majorité des processeurs superscalaires a plus de ports d'émission que de décodage. Expliquer pourquoi va cependant nous demander de parler de comment la superscalarité exploite les unités de calcul.
Posons-nous cette question : de combien d'unités un processeur superscalaire a besoin ? Les unités en question ne sont nécessairement des unité de calcul, l'unité mémoire et l'unité de branchement comptent aussi. La réponse dépend du nombre de voies. Si le ''front-end'' charge et décode un paquet de N instructions, exécuter ces N instructions simultanément demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section.
La réponse dépend aussi des contraintes d’appariement. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission parfaite devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement, ce qui ferait passer de 6 unités à 12. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, le cout en circuit encore plus prohibitif !
En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée, alors que la double émission parait plus adaptée.
Et c'est là que la différence entre ports d'émission et de décodage devient intéressante. L'idée est d'avoir une unité d'émission avec deux ports de décodage, et 4 port d'émission (un par unité). Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux micro-opérations tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu.
Les tout premiers CPU superscalaires étaient nombreux à utiliser cette technique. Les exemples classiques sont l'HyperSPARC, de l'alpha 21064 et du PowerPC 603. C'étaient des CPU double émission, avec une ALU entière, une unité mémoire et une FPU. Nous parlerons de '''partitionnement INT-FLOAT-MEM'''. Les contraintes d’appariement variaient grandement suivant le processeur.
Le PowerPC 603 implémentait une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur.
Avant de poursuivre, précisons que la FPU regroupe un additionneur flottant et un multiplieur flottant. Et ce fait peut être exploité pour approfondir le partitionnement. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions).
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple, facile à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. La raison est que le partitionnement permet d'émettre des paires, triplets, quadruplets de micro-opérations qui sont rares en pratique.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''.
Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, 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. Et enfin, il faut rajouter un port d'émission pour pouvoir émettre une micro-opération entière en plus.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle.
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
Reste à connecter les unités de calcul à l'unité d'émission (indirectement, car il y a des étages de pipeline entre les deux). Une implémentation basique utilisee un port d'émission par unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La présence d'un circuit multiplieur et d'un ''barrel shifter'' pose des problèmes pour le partitionnement. Il est possible d'avoir un port d'émission dédié au multiplieur, et un autre au ''barrel shifter'', qui sont séparés de ceux pour l'ALU entière. Il est alors possible d'émettre une opération entière, une multiplication et un décalage, simultanément. Nous parlerons de '''partitionnement entier''' pour désigner cette possibilité.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
Le partitionnement entier est cependant très rare, car le gain en performance serait trop faible au regard des couts. Quelques processeurs historiques l'utilisaient, mais ce n'est plus le cas de nos jours. La raison est que les multiplications et les décalages sont assez rares dans le code, ce qui fait que le port d'émission associés sont sous-utilisés. Et un port d'émission a un cout en circuit assez important. 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.
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul. L'exemple classique est justement lié aux circuits multiplieur et ''barrel shifters''. L'idée est que le multiplieur et le ''barrel shifter'' sont mis sur le même port d'émission qu'une ALU entière.
Prenons par exemple le processeur à double émission entière illustré ci-contre. Un port d'émission est relié à une ALU entière seule, et l'autre port d'émission alimente la seconde ALU entière et le multiplieur. La conséquence est que le processeur peut émettre deux opérations entières, dont une multiplication. Si on avait utilisé un port d'émission dédié au multiplieur, il aurait été possible d'émettre deux opérations entières et une multiplication. On perd donc une voie, mais on gagne en flexibilité et on économise des circuits.
[[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]]
En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais le partage des ports d'émission peut appliquer à n'importe quelle unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. 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.
La solution est de partager des ports d'émission, pour passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple relie une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires mélangent les trois techniques===
Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autre unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents.
* Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU.
* La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants.
* La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables.
* Le '''partage des ports d'émission''' réduit le cout en circuit du CPU, pour des pertes de performances mineures si bien fait.
De nos jours, le partitionnement est utilisé en complément de la duplication des ALU entières, de la duplication des autres unités, et du partage des ports d'émission. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. Le partage des ports d'émission a lui aussi mis un peu de temps pour s'imposer.
Prenons l'exemple des processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
==L'évolution historique de la superscalarité==
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. La plupart des CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 sont rapidement montés à de la quadruple émission, avec quelques exceptions. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 ||
|-
! 1995
| 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000
|-
! 1996
| class="f_bleu | 620 || || || || class="f_bleu | 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
===Les processeurs historiques à double émission===
Pour commencer, étudions le cas des premiers CPU superscalaires à double émission. Ils sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Notamment, la duplication des autres unités ne sert pour ainsi dire à rien, ce qui limite les possibilités, mais rendra les explications plus simples.
Il pouvaient se classer en trois types principaux, avec quelques processeurs qui ne rentraient pas dans ces cases. Le premier type utilise un ''partitionnement complet'' entre ALU, FPU et unité mémoire. Le second utilisait la double émission entière-flottante. Le troisième utilisait la double émission entière. Voici le tableau chronologique disant quel CPU utilisait quelle technique.
* Le partitionnement INT-FLOAT-MEM est en jaune.
* La double émission entière-flottante est en vert.
* La double émission entière est en rouge.
* Les CPU barrés utilisent la quadruple émission et seront vu dans la section suivante.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| class="f_vert | RS/6000 || || || class="f_rouge | i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| class="f_vert | RSC || class="f_vert | PA 7100 || SuperSPARC || || class="f_jaune | 21064 ||
|-
! 1993
| class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || ||
|-
! 1994
| <s>POWER 2</s> || PA 7200 || || || <s>21164</s> ||
|}
Les processeurs laissés de côté dans le tableau précédent ne rentrent pas dans les catégories précédentes.
Le PA 7200 est un PA-7100 auquel on aurait rajouté une ALU entière. Le processeur exécutait les multiplications entières dans la FPU, comme son prédécesseur. Les combinaisons autorisées étaient donc INT + INT, INT + MEM, INT + FLOAT/MUL, MEM + FLOAT/MUL.
Le SuperSPARC était un CPU triple émission, avec deux ALU entières, une FPU et une unité mémoire. Il avait de nombreuses contraintes d’appariement. Par exemple, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Une bonne partie de ces contraintes était lié au banc de registre, qui n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions. Il faut noter que la seconde ALU a été abandonnée lors du passage au CPU HyperSPARC, qui est de plus repassé en double émission, ce qui est une évolution à rebours de l'évolution des autres CPUs.
===Les processeurs historiques à quadruple émission===
C'est la même année , en 1994, que la quadruple émission est arrivée et que les CPU ont commencé à avoir de plus en plus d'unités de calcul. Même les designs restés en double émission avaient ajouté des unités de calcul. Il y avait alors trois stratégies pour la quadruple émission.
La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Les FPUs étaient soit dupliquées, soit scindées en deux en séparant l'additionneur du multiplieur flottant. Le POWER 2 dupliquait ses FPUs à la perfection, alors que l'21264 partitionnait la FPU. Les accès mémoire étaient traités comme des opérations entières, et le processeur pouvait : soit en faire un seul par cycle, soit en émettre deux, sous conditions (le cache devait être double port pour lire/écrire deux données à la fois, comme on le verra dans la suite du chapitre).
Les processeurs MIPS R8000 et R10000 allaient un peu plus loin, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités.
La seconde implémentation dupliquait l'ALU entière et combinait le tout avec un partitionnement INT-FLOAT-MEM. De quoi exécuter en même temps deux opérations entières, une opération flottante, et un accès mémoire. 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).
Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, avec au minimum deux ALU entières. Pour comprendre ce que je veux dire, prenons l'exemple de l'UltraSPARC. Il avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplier flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes. Le processeur pouvait émettre 4 instructions, tant qu'elles atterrissaient dans des unités différentes.
La même méthode peut être utilisée pour la double émission. Le Motorola 88110, sorti en 1991, était un CPU à double émission avec pas moins de 10 unités : deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise.
La morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes. Le tout est résumé dans le tableau ci-dessous, avec :
* en vert les CPU utilisant la première stratégie ;
* en rouge les CPU utilisant la seconde stratégie ;
* en jaune les CPU utilisant la troisième stratégie.
{|class="wikitable"
|+ Processeurs quadruple émission historiques
|-
! Année !! Processeur !! Unités
|-
! 1991
| class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques
|-
|-
! rowspan="2" | 1994
| class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'')
|-
| class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM
|-
! rowspan="3" | 1995
| class="f_jaune | PA 8000 || class="f_jaune | 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD
|-
| class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques
|-
| MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM
|-
! rowspan="2" | 1996
| class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM
|-
| class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL)
|}
===Les processeurs superscalaires modernes===
Les CPU superscalaires modernes ont des design légèrement différents des CPU historiques. Ils dupliquent l'unité entière, mais leur partitionnement est différent. En général, les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Par exemple, un CPU à 6 voies pourra émettre/exécuter 6 opérations entières simultanément, grâce à 6 ALU entières. Mais il s'agit là d'un maximum et certaines peuvent être remplacées par une opération flottante ou mémoire, qui utilisent l'unité mémoire et/ou la FPU.
Un exemple assez parlant est les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon. C'était une microarchitecture à triple émission, avec trois ALU entières et trois unités mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements.
Il s'agit là d'une tendance, cependant. Les CPU superscalaires tendent à mettre le paquet niveau opérations arithmétiques, mais pas forcément au point de remplir la totalité de la largeur du processeur. La règle est que pour N voies, le processeur dispose d'au moins N/2 ALU entières. Attention, cela ne signifie pas que le processeur émet systématiquement N/2 opération entières. Il peut par exemple émettre une combinaison d'opérations flottantes et mémoire, avec des branchements. Et N/2 est une borne basse, la plupart des CPU x86 ont environ tournent plutôt autour des 2/3, voire des 3/4.
En général, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. La conséquence est que les unités mémoire sont dupliquées, en seconde place dans l'ordre d'importance. Les processeurs x86 ont commencé à dupliquer leurs unités mémoire dès qu'ils ont atteint la quadruple émission, pour les CPU grand public. La microarchitecture Core d'Intel utilisait la quadruple émission, avec deux unités mémoire et trois ALU entières. Les microarchitectures d'AMD K5 et K6 utilisaient aussi la quadruple émission et avaient deux ALUs pour deux unités mémoire.
Plus récemment, la microarchitecture ''Golden Cove'' dispose de cinq unités de calcul trois unités mémoire (deux spécialisées dans les écritures, trois dans les lectures). La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire.Il y a donc soit égalité, soit avantage pour les ALU entières.
Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Le banc de registre d'un processeur superscalaire===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Une autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage.
===Les unités d'exécution dans le désordre===
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions auto-aligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions auto-aligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez longs, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
m961ctbzp957ggrulje2hm8jrquiuqk
773177
773176
2026-09-25T20:16:39Z
Mewtow
31375
/* Les autres étages de pipeline d'un CPU superscalaire */
773177
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===L'impact des contraintes d’appariement sur la performance===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour la suite du chapitre===
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==Le ''front-end'' des processeurs superscalaires==
Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple.
Intuitivement, charger, décoder et 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 certaines processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. 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
|}
Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs.
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===L'émission parallèle des instructions===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elle sont propagée au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage).
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
==Les processeurs implémentés avec plusieurs pipelines==
Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Vous vous dites que l'implémentation d'un processeur superscalaire n'est pas la même suivant que le CPU utilise l'exécution dans le désordre, ou non. Mais dans les faits, la distinction est plus subtile que ca. Pour l'expliquer, je vais utiliser une classification originale, qui sépare les processeurs superscalaires en trois sous-types. La distinction se fait sur la manière dont la superscalarité est implémentée en matériel.La classification que j'utilise distingue :
* Les processeurs avec plusieurs pipelines, sans exécution dans le désordre.
* Les processeurs superscalaires avec un pipeline dynamique.
* Les processeurs avec des fenêtres d'instruction décentralisées.
La distinction n'est pas qu'une question d'implémentation matérielle. Elle impacte aussi le ratio entre instructions chargées/décodées et instructions émises. Vous vous dites que les deux sont égales, mais ce n'est le cas que sur les processeurs avec N pipelines ! Sur les autres processeurs, il est possible de décoder 4 instructions, mais d'en émettre maximum 6 par cycle ! Et sur les CPU avec des fenêtres d'instruction décentralisées, il y a aussi une différence entre le nombre d'instructions qui sont ''dispatch'' et ''schedule''.
La catégorie la plus simple est celle des '''processeurs avec plusieurs pipelines'''. Ce sont des processeurs superscalaires sans exécution dans le désordre. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction.
Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, d'une FPU, éventuellement d'un second banc de registres, guère plus. Il permet d'exécuter une seconde opération de calcul, pas plus, ce qui est à l'origine de contraintes d'appariemment assez fortes. De plus, il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section.
===La double émission entière-flottante===
L'exemple le plus simple utilise un pipeline pour les opérations flottantes, séparé du pipeline originel. Pour bien expliquer les choses, prenons le processeur illustré ci-dessous. Un processeur a deux ''back-end'' séparés, un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par l'ALU entière, passent éventuellement dans l'unité mémoire, pour ensuite arriver à l'étape de ''writeback''. La FPU et le banc de registre flottant ont eux aussi leur propre ''back-end'' séparé.
[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]]
Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaqaue pipeline, à chaque cycle.
L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine.
Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants.
===La double émission entière===
Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait.
Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
==Les processeurs superscalaires dynamiques==
Les processeurs précédents, avec 2 pipelines, avaient une unité d'émission assez simple. Elle avait deux ports de décodage, et deux ports d'émission. Le nombre de ports de décodage et d'émission étaient identiques. Vous vous dites que c'est naturel, et intuitif, que c'est normal pour un processeur superscalaire. Mais en réalité, la majorité des processeurs superscalaires a plus de ports d'émission que de décodage. Expliquer pourquoi va cependant nous demander de parler de comment la superscalarité exploite les unités de calcul.
Posons-nous cette question : de combien d'unités un processeur superscalaire a besoin ? Les unités en question ne sont nécessairement des unité de calcul, l'unité mémoire et l'unité de branchement comptent aussi. La réponse dépend du nombre de voies. Si le ''front-end'' charge et décode un paquet de N instructions, exécuter ces N instructions simultanément demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section.
La réponse dépend aussi des contraintes d’appariement. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission parfaite devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement, ce qui ferait passer de 6 unités à 12. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, le cout en circuit encore plus prohibitif !
En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée, alors que la double émission parait plus adaptée.
Et c'est là que la différence entre ports d'émission et de décodage devient intéressante. L'idée est d'avoir une unité d'émission avec deux ports de décodage, et 4 port d'émission (un par unité). Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux micro-opérations tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu.
Les tout premiers CPU superscalaires étaient nombreux à utiliser cette technique. Les exemples classiques sont l'HyperSPARC, de l'alpha 21064 et du PowerPC 603. C'étaient des CPU double émission, avec une ALU entière, une unité mémoire et une FPU. Nous parlerons de '''partitionnement INT-FLOAT-MEM'''. Les contraintes d’appariement variaient grandement suivant le processeur.
Le PowerPC 603 implémentait une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur.
Avant de poursuivre, précisons que la FPU regroupe un additionneur flottant et un multiplieur flottant. Et ce fait peut être exploité pour approfondir le partitionnement. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions).
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple, facile à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. La raison est que le partitionnement permet d'émettre des paires, triplets, quadruplets de micro-opérations qui sont rares en pratique.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''.
Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, 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. Et enfin, il faut rajouter un port d'émission pour pouvoir émettre une micro-opération entière en plus.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle.
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
Reste à connecter les unités de calcul à l'unité d'émission (indirectement, car il y a des étages de pipeline entre les deux). Une implémentation basique utilisee un port d'émission par unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La présence d'un circuit multiplieur et d'un ''barrel shifter'' pose des problèmes pour le partitionnement. Il est possible d'avoir un port d'émission dédié au multiplieur, et un autre au ''barrel shifter'', qui sont séparés de ceux pour l'ALU entière. Il est alors possible d'émettre une opération entière, une multiplication et un décalage, simultanément. Nous parlerons de '''partitionnement entier''' pour désigner cette possibilité.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
Le partitionnement entier est cependant très rare, car le gain en performance serait trop faible au regard des couts. Quelques processeurs historiques l'utilisaient, mais ce n'est plus le cas de nos jours. La raison est que les multiplications et les décalages sont assez rares dans le code, ce qui fait que le port d'émission associés sont sous-utilisés. Et un port d'émission a un cout en circuit assez important. 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.
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul. L'exemple classique est justement lié aux circuits multiplieur et ''barrel shifters''. L'idée est que le multiplieur et le ''barrel shifter'' sont mis sur le même port d'émission qu'une ALU entière.
Prenons par exemple le processeur à double émission entière illustré ci-contre. Un port d'émission est relié à une ALU entière seule, et l'autre port d'émission alimente la seconde ALU entière et le multiplieur. La conséquence est que le processeur peut émettre deux opérations entières, dont une multiplication. Si on avait utilisé un port d'émission dédié au multiplieur, il aurait été possible d'émettre deux opérations entières et une multiplication. On perd donc une voie, mais on gagne en flexibilité et on économise des circuits.
[[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]]
En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais le partage des ports d'émission peut appliquer à n'importe quelle unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. 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.
La solution est de partager des ports d'émission, pour passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple relie une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires mélangent les trois techniques===
Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autre unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents.
* Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU.
* La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants.
* La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables.
* Le '''partage des ports d'émission''' réduit le cout en circuit du CPU, pour des pertes de performances mineures si bien fait.
De nos jours, le partitionnement est utilisé en complément de la duplication des ALU entières, de la duplication des autres unités, et du partage des ports d'émission. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. Le partage des ports d'émission a lui aussi mis un peu de temps pour s'imposer.
Prenons l'exemple des processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
==L'évolution historique de la superscalarité==
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. La plupart des CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 sont rapidement montés à de la quadruple émission, avec quelques exceptions. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 ||
|-
! 1995
| 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000
|-
! 1996
| class="f_bleu | 620 || || || || class="f_bleu | 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
===Les processeurs historiques à double émission===
Pour commencer, étudions le cas des premiers CPU superscalaires à double émission. Ils sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Notamment, la duplication des autres unités ne sert pour ainsi dire à rien, ce qui limite les possibilités, mais rendra les explications plus simples.
Il pouvaient se classer en trois types principaux, avec quelques processeurs qui ne rentraient pas dans ces cases. Le premier type utilise un ''partitionnement complet'' entre ALU, FPU et unité mémoire. Le second utilisait la double émission entière-flottante. Le troisième utilisait la double émission entière. Voici le tableau chronologique disant quel CPU utilisait quelle technique.
* Le partitionnement INT-FLOAT-MEM est en jaune.
* La double émission entière-flottante est en vert.
* La double émission entière est en rouge.
* Les CPU barrés utilisent la quadruple émission et seront vu dans la section suivante.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| class="f_vert | RS/6000 || || || class="f_rouge | i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| class="f_vert | RSC || class="f_vert | PA 7100 || SuperSPARC || || class="f_jaune | 21064 ||
|-
! 1993
| class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || ||
|-
! 1994
| <s>POWER 2</s> || PA 7200 || || || <s>21164</s> ||
|}
Les processeurs laissés de côté dans le tableau précédent ne rentrent pas dans les catégories précédentes.
Le PA 7200 est un PA-7100 auquel on aurait rajouté une ALU entière. Le processeur exécutait les multiplications entières dans la FPU, comme son prédécesseur. Les combinaisons autorisées étaient donc INT + INT, INT + MEM, INT + FLOAT/MUL, MEM + FLOAT/MUL.
Le SuperSPARC était un CPU triple émission, avec deux ALU entières, une FPU et une unité mémoire. Il avait de nombreuses contraintes d’appariement. Par exemple, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Une bonne partie de ces contraintes était lié au banc de registre, qui n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions. Il faut noter que la seconde ALU a été abandonnée lors du passage au CPU HyperSPARC, qui est de plus repassé en double émission, ce qui est une évolution à rebours de l'évolution des autres CPUs.
===Les processeurs historiques à quadruple émission===
C'est la même année , en 1994, que la quadruple émission est arrivée et que les CPU ont commencé à avoir de plus en plus d'unités de calcul. Même les designs restés en double émission avaient ajouté des unités de calcul. Il y avait alors trois stratégies pour la quadruple émission.
La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Les FPUs étaient soit dupliquées, soit scindées en deux en séparant l'additionneur du multiplieur flottant. Le POWER 2 dupliquait ses FPUs à la perfection, alors que l'21264 partitionnait la FPU. Les accès mémoire étaient traités comme des opérations entières, et le processeur pouvait : soit en faire un seul par cycle, soit en émettre deux, sous conditions (le cache devait être double port pour lire/écrire deux données à la fois, comme on le verra dans la suite du chapitre).
Les processeurs MIPS R8000 et R10000 allaient un peu plus loin, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités.
La seconde implémentation dupliquait l'ALU entière et combinait le tout avec un partitionnement INT-FLOAT-MEM. De quoi exécuter en même temps deux opérations entières, une opération flottante, et un accès mémoire. 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).
Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, avec au minimum deux ALU entières. Pour comprendre ce que je veux dire, prenons l'exemple de l'UltraSPARC. Il avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplier flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes. Le processeur pouvait émettre 4 instructions, tant qu'elles atterrissaient dans des unités différentes.
La même méthode peut être utilisée pour la double émission. Le Motorola 88110, sorti en 1991, était un CPU à double émission avec pas moins de 10 unités : deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise.
La morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes. Le tout est résumé dans le tableau ci-dessous, avec :
* en vert les CPU utilisant la première stratégie ;
* en rouge les CPU utilisant la seconde stratégie ;
* en jaune les CPU utilisant la troisième stratégie.
{|class="wikitable"
|+ Processeurs quadruple émission historiques
|-
! Année !! Processeur !! Unités
|-
! 1991
| class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques
|-
|-
! rowspan="2" | 1994
| class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'')
|-
| class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM
|-
! rowspan="3" | 1995
| class="f_jaune | PA 8000 || class="f_jaune | 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD
|-
| class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques
|-
| MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM
|-
! rowspan="2" | 1996
| class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM
|-
| class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL)
|}
===Les processeurs superscalaires modernes===
Les CPU superscalaires modernes ont des design légèrement différents des CPU historiques. Ils dupliquent l'unité entière, mais leur partitionnement est différent. En général, les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Par exemple, un CPU à 6 voies pourra émettre/exécuter 6 opérations entières simultanément, grâce à 6 ALU entières. Mais il s'agit là d'un maximum et certaines peuvent être remplacées par une opération flottante ou mémoire, qui utilisent l'unité mémoire et/ou la FPU.
Un exemple assez parlant est les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon. C'était une microarchitecture à triple émission, avec trois ALU entières et trois unités mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements.
Il s'agit là d'une tendance, cependant. Les CPU superscalaires tendent à mettre le paquet niveau opérations arithmétiques, mais pas forcément au point de remplir la totalité de la largeur du processeur. La règle est que pour N voies, le processeur dispose d'au moins N/2 ALU entières. Attention, cela ne signifie pas que le processeur émet systématiquement N/2 opération entières. Il peut par exemple émettre une combinaison d'opérations flottantes et mémoire, avec des branchements. Et N/2 est une borne basse, la plupart des CPU x86 ont environ tournent plutôt autour des 2/3, voire des 3/4.
En général, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. La conséquence est que les unités mémoire sont dupliquées, en seconde place dans l'ordre d'importance. Les processeurs x86 ont commencé à dupliquer leurs unités mémoire dès qu'ils ont atteint la quadruple émission, pour les CPU grand public. La microarchitecture Core d'Intel utilisait la quadruple émission, avec deux unités mémoire et trois ALU entières. Les microarchitectures d'AMD K5 et K6 utilisaient aussi la quadruple émission et avaient deux ALUs pour deux unités mémoire.
Plus récemment, la microarchitecture ''Golden Cove'' dispose de cinq unités de calcul trois unités mémoire (deux spécialisées dans les écritures, trois dans les lectures). La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire.Il y a donc soit égalité, soit avantage pour les ALU entières.
Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les CPU superscalaire à exécution dans le désordre==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Le banc de registre d'un processeur superscalaire===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Une autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage.
===Les unités d'exécution dans le désordre===
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions auto-aligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions auto-aligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez longs, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
mnroshm2xq9ectfo28i96lxup19ueyt
773178
773177
2026-09-25T20:16:57Z
Mewtow
31375
/* Le banc de registre d'un processeur superscalaire */
773178
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===L'impact des contraintes d’appariement sur la performance===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour la suite du chapitre===
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==Le ''front-end'' des processeurs superscalaires==
Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple.
Intuitivement, charger, décoder et 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 certaines processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. 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
|}
Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs.
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===L'émission parallèle des instructions===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elle sont propagée au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage).
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
==Les processeurs implémentés avec plusieurs pipelines==
Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Vous vous dites que l'implémentation d'un processeur superscalaire n'est pas la même suivant que le CPU utilise l'exécution dans le désordre, ou non. Mais dans les faits, la distinction est plus subtile que ca. Pour l'expliquer, je vais utiliser une classification originale, qui sépare les processeurs superscalaires en trois sous-types. La distinction se fait sur la manière dont la superscalarité est implémentée en matériel.La classification que j'utilise distingue :
* Les processeurs avec plusieurs pipelines, sans exécution dans le désordre.
* Les processeurs superscalaires avec un pipeline dynamique.
* Les processeurs avec des fenêtres d'instruction décentralisées.
La distinction n'est pas qu'une question d'implémentation matérielle. Elle impacte aussi le ratio entre instructions chargées/décodées et instructions émises. Vous vous dites que les deux sont égales, mais ce n'est le cas que sur les processeurs avec N pipelines ! Sur les autres processeurs, il est possible de décoder 4 instructions, mais d'en émettre maximum 6 par cycle ! Et sur les CPU avec des fenêtres d'instruction décentralisées, il y a aussi une différence entre le nombre d'instructions qui sont ''dispatch'' et ''schedule''.
La catégorie la plus simple est celle des '''processeurs avec plusieurs pipelines'''. Ce sont des processeurs superscalaires sans exécution dans le désordre. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction.
Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, d'une FPU, éventuellement d'un second banc de registres, guère plus. Il permet d'exécuter une seconde opération de calcul, pas plus, ce qui est à l'origine de contraintes d'appariemment assez fortes. De plus, il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section.
===La double émission entière-flottante===
L'exemple le plus simple utilise un pipeline pour les opérations flottantes, séparé du pipeline originel. Pour bien expliquer les choses, prenons le processeur illustré ci-dessous. Un processeur a deux ''back-end'' séparés, un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par l'ALU entière, passent éventuellement dans l'unité mémoire, pour ensuite arriver à l'étape de ''writeback''. La FPU et le banc de registre flottant ont eux aussi leur propre ''back-end'' séparé.
[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]]
Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaqaue pipeline, à chaque cycle.
L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine.
Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants.
===La double émission entière===
Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait.
Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
==Les processeurs superscalaires dynamiques==
Les processeurs précédents, avec 2 pipelines, avaient une unité d'émission assez simple. Elle avait deux ports de décodage, et deux ports d'émission. Le nombre de ports de décodage et d'émission étaient identiques. Vous vous dites que c'est naturel, et intuitif, que c'est normal pour un processeur superscalaire. Mais en réalité, la majorité des processeurs superscalaires a plus de ports d'émission que de décodage. Expliquer pourquoi va cependant nous demander de parler de comment la superscalarité exploite les unités de calcul.
Posons-nous cette question : de combien d'unités un processeur superscalaire a besoin ? Les unités en question ne sont nécessairement des unité de calcul, l'unité mémoire et l'unité de branchement comptent aussi. La réponse dépend du nombre de voies. Si le ''front-end'' charge et décode un paquet de N instructions, exécuter ces N instructions simultanément demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section.
La réponse dépend aussi des contraintes d’appariement. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission parfaite devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement, ce qui ferait passer de 6 unités à 12. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, le cout en circuit encore plus prohibitif !
En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée, alors que la double émission parait plus adaptée.
Et c'est là que la différence entre ports d'émission et de décodage devient intéressante. L'idée est d'avoir une unité d'émission avec deux ports de décodage, et 4 port d'émission (un par unité). Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux micro-opérations tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu.
Les tout premiers CPU superscalaires étaient nombreux à utiliser cette technique. Les exemples classiques sont l'HyperSPARC, de l'alpha 21064 et du PowerPC 603. C'étaient des CPU double émission, avec une ALU entière, une unité mémoire et une FPU. Nous parlerons de '''partitionnement INT-FLOAT-MEM'''. Les contraintes d’appariement variaient grandement suivant le processeur.
Le PowerPC 603 implémentait une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur.
Avant de poursuivre, précisons que la FPU regroupe un additionneur flottant et un multiplieur flottant. Et ce fait peut être exploité pour approfondir le partitionnement. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions).
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple, facile à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. La raison est que le partitionnement permet d'émettre des paires, triplets, quadruplets de micro-opérations qui sont rares en pratique.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''.
Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, 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. Et enfin, il faut rajouter un port d'émission pour pouvoir émettre une micro-opération entière en plus.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle.
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
Reste à connecter les unités de calcul à l'unité d'émission (indirectement, car il y a des étages de pipeline entre les deux). Une implémentation basique utilisee un port d'émission par unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La présence d'un circuit multiplieur et d'un ''barrel shifter'' pose des problèmes pour le partitionnement. Il est possible d'avoir un port d'émission dédié au multiplieur, et un autre au ''barrel shifter'', qui sont séparés de ceux pour l'ALU entière. Il est alors possible d'émettre une opération entière, une multiplication et un décalage, simultanément. Nous parlerons de '''partitionnement entier''' pour désigner cette possibilité.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
Le partitionnement entier est cependant très rare, car le gain en performance serait trop faible au regard des couts. Quelques processeurs historiques l'utilisaient, mais ce n'est plus le cas de nos jours. La raison est que les multiplications et les décalages sont assez rares dans le code, ce qui fait que le port d'émission associés sont sous-utilisés. Et un port d'émission a un cout en circuit assez important. 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.
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul. L'exemple classique est justement lié aux circuits multiplieur et ''barrel shifters''. L'idée est que le multiplieur et le ''barrel shifter'' sont mis sur le même port d'émission qu'une ALU entière.
Prenons par exemple le processeur à double émission entière illustré ci-contre. Un port d'émission est relié à une ALU entière seule, et l'autre port d'émission alimente la seconde ALU entière et le multiplieur. La conséquence est que le processeur peut émettre deux opérations entières, dont une multiplication. Si on avait utilisé un port d'émission dédié au multiplieur, il aurait été possible d'émettre deux opérations entières et une multiplication. On perd donc une voie, mais on gagne en flexibilité et on économise des circuits.
[[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]]
En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais le partage des ports d'émission peut appliquer à n'importe quelle unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. 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.
La solution est de partager des ports d'émission, pour passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple relie une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires mélangent les trois techniques===
Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autre unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents.
* Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU.
* La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants.
* La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables.
* Le '''partage des ports d'émission''' réduit le cout en circuit du CPU, pour des pertes de performances mineures si bien fait.
De nos jours, le partitionnement est utilisé en complément de la duplication des ALU entières, de la duplication des autres unités, et du partage des ports d'émission. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. Le partage des ports d'émission a lui aussi mis un peu de temps pour s'imposer.
Prenons l'exemple des processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
==L'évolution historique de la superscalarité==
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. La plupart des CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 sont rapidement montés à de la quadruple émission, avec quelques exceptions. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 ||
|-
! 1995
| 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000
|-
! 1996
| class="f_bleu | 620 || || || || class="f_bleu | 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
===Les processeurs historiques à double émission===
Pour commencer, étudions le cas des premiers CPU superscalaires à double émission. Ils sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Notamment, la duplication des autres unités ne sert pour ainsi dire à rien, ce qui limite les possibilités, mais rendra les explications plus simples.
Il pouvaient se classer en trois types principaux, avec quelques processeurs qui ne rentraient pas dans ces cases. Le premier type utilise un ''partitionnement complet'' entre ALU, FPU et unité mémoire. Le second utilisait la double émission entière-flottante. Le troisième utilisait la double émission entière. Voici le tableau chronologique disant quel CPU utilisait quelle technique.
* Le partitionnement INT-FLOAT-MEM est en jaune.
* La double émission entière-flottante est en vert.
* La double émission entière est en rouge.
* Les CPU barrés utilisent la quadruple émission et seront vu dans la section suivante.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| class="f_vert | RS/6000 || || || class="f_rouge | i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| class="f_vert | RSC || class="f_vert | PA 7100 || SuperSPARC || || class="f_jaune | 21064 ||
|-
! 1993
| class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || ||
|-
! 1994
| <s>POWER 2</s> || PA 7200 || || || <s>21164</s> ||
|}
Les processeurs laissés de côté dans le tableau précédent ne rentrent pas dans les catégories précédentes.
Le PA 7200 est un PA-7100 auquel on aurait rajouté une ALU entière. Le processeur exécutait les multiplications entières dans la FPU, comme son prédécesseur. Les combinaisons autorisées étaient donc INT + INT, INT + MEM, INT + FLOAT/MUL, MEM + FLOAT/MUL.
Le SuperSPARC était un CPU triple émission, avec deux ALU entières, une FPU et une unité mémoire. Il avait de nombreuses contraintes d’appariement. Par exemple, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Une bonne partie de ces contraintes était lié au banc de registre, qui n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions. Il faut noter que la seconde ALU a été abandonnée lors du passage au CPU HyperSPARC, qui est de plus repassé en double émission, ce qui est une évolution à rebours de l'évolution des autres CPUs.
===Les processeurs historiques à quadruple émission===
C'est la même année , en 1994, que la quadruple émission est arrivée et que les CPU ont commencé à avoir de plus en plus d'unités de calcul. Même les designs restés en double émission avaient ajouté des unités de calcul. Il y avait alors trois stratégies pour la quadruple émission.
La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Les FPUs étaient soit dupliquées, soit scindées en deux en séparant l'additionneur du multiplieur flottant. Le POWER 2 dupliquait ses FPUs à la perfection, alors que l'21264 partitionnait la FPU. Les accès mémoire étaient traités comme des opérations entières, et le processeur pouvait : soit en faire un seul par cycle, soit en émettre deux, sous conditions (le cache devait être double port pour lire/écrire deux données à la fois, comme on le verra dans la suite du chapitre).
Les processeurs MIPS R8000 et R10000 allaient un peu plus loin, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités.
La seconde implémentation dupliquait l'ALU entière et combinait le tout avec un partitionnement INT-FLOAT-MEM. De quoi exécuter en même temps deux opérations entières, une opération flottante, et un accès mémoire. 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).
Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, avec au minimum deux ALU entières. Pour comprendre ce que je veux dire, prenons l'exemple de l'UltraSPARC. Il avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplier flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes. Le processeur pouvait émettre 4 instructions, tant qu'elles atterrissaient dans des unités différentes.
La même méthode peut être utilisée pour la double émission. Le Motorola 88110, sorti en 1991, était un CPU à double émission avec pas moins de 10 unités : deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise.
La morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes. Le tout est résumé dans le tableau ci-dessous, avec :
* en vert les CPU utilisant la première stratégie ;
* en rouge les CPU utilisant la seconde stratégie ;
* en jaune les CPU utilisant la troisième stratégie.
{|class="wikitable"
|+ Processeurs quadruple émission historiques
|-
! Année !! Processeur !! Unités
|-
! 1991
| class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques
|-
|-
! rowspan="2" | 1994
| class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'')
|-
| class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM
|-
! rowspan="3" | 1995
| class="f_jaune | PA 8000 || class="f_jaune | 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD
|-
| class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques
|-
| MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM
|-
! rowspan="2" | 1996
| class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM
|-
| class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL)
|}
===Les processeurs superscalaires modernes===
Les CPU superscalaires modernes ont des design légèrement différents des CPU historiques. Ils dupliquent l'unité entière, mais leur partitionnement est différent. En général, les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Par exemple, un CPU à 6 voies pourra émettre/exécuter 6 opérations entières simultanément, grâce à 6 ALU entières. Mais il s'agit là d'un maximum et certaines peuvent être remplacées par une opération flottante ou mémoire, qui utilisent l'unité mémoire et/ou la FPU.
Un exemple assez parlant est les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon. C'était une microarchitecture à triple émission, avec trois ALU entières et trois unités mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements.
Il s'agit là d'une tendance, cependant. Les CPU superscalaires tendent à mettre le paquet niveau opérations arithmétiques, mais pas forcément au point de remplir la totalité de la largeur du processeur. La règle est que pour N voies, le processeur dispose d'au moins N/2 ALU entières. Attention, cela ne signifie pas que le processeur émet systématiquement N/2 opération entières. Il peut par exemple émettre une combinaison d'opérations flottantes et mémoire, avec des branchements. Et N/2 est une borne basse, la plupart des CPU x86 ont environ tournent plutôt autour des 2/3, voire des 3/4.
En général, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. La conséquence est que les unités mémoire sont dupliquées, en seconde place dans l'ordre d'importance. Les processeurs x86 ont commencé à dupliquer leurs unités mémoire dès qu'ils ont atteint la quadruple émission, pour les CPU grand public. La microarchitecture Core d'Intel utilisait la quadruple émission, avec deux unités mémoire et trois ALU entières. Les microarchitectures d'AMD K5 et K6 utilisaient aussi la quadruple émission et avaient deux ALUs pour deux unités mémoire.
Plus récemment, la microarchitecture ''Golden Cove'' dispose de cinq unités de calcul trois unités mémoire (deux spécialisées dans les écritures, trois dans les lectures). La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire.Il y a donc soit égalité, soit avantage pour les ALU entières.
Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les CPU superscalaire à exécution dans le désordre==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Les unités d'exécution dans le désordre===
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions auto-aligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions auto-aligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez longs, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
oqttzyo5vt31t8phsvvezyoimc7497z
773179
773178
2026-09-25T20:20:11Z
Mewtow
31375
/* Le banc de registre des processeurs superscalaires larges */
773179
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===L'impact des contraintes d’appariement sur la performance===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour la suite du chapitre===
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==Le ''front-end'' des processeurs superscalaires==
Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple.
Intuitivement, charger, décoder et 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 certaines processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. 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
|}
Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs.
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===L'émission parallèle des instructions===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elle sont propagée au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage).
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
==Les processeurs implémentés avec plusieurs pipelines==
Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Vous vous dites que l'implémentation d'un processeur superscalaire n'est pas la même suivant que le CPU utilise l'exécution dans le désordre, ou non. Mais dans les faits, la distinction est plus subtile que ca. Pour l'expliquer, je vais utiliser une classification originale, qui sépare les processeurs superscalaires en trois sous-types. La distinction se fait sur la manière dont la superscalarité est implémentée en matériel.La classification que j'utilise distingue :
* Les processeurs avec plusieurs pipelines, sans exécution dans le désordre.
* Les processeurs superscalaires avec un pipeline dynamique.
* Les processeurs avec des fenêtres d'instruction décentralisées.
La distinction n'est pas qu'une question d'implémentation matérielle. Elle impacte aussi le ratio entre instructions chargées/décodées et instructions émises. Vous vous dites que les deux sont égales, mais ce n'est le cas que sur les processeurs avec N pipelines ! Sur les autres processeurs, il est possible de décoder 4 instructions, mais d'en émettre maximum 6 par cycle ! Et sur les CPU avec des fenêtres d'instruction décentralisées, il y a aussi une différence entre le nombre d'instructions qui sont ''dispatch'' et ''schedule''.
La catégorie la plus simple est celle des '''processeurs avec plusieurs pipelines'''. Ce sont des processeurs superscalaires sans exécution dans le désordre. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction.
Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, d'une FPU, éventuellement d'un second banc de registres, guère plus. Il permet d'exécuter une seconde opération de calcul, pas plus, ce qui est à l'origine de contraintes d'appariemment assez fortes. De plus, il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section.
===La double émission entière-flottante===
L'exemple le plus simple utilise un pipeline pour les opérations flottantes, séparé du pipeline originel. Pour bien expliquer les choses, prenons le processeur illustré ci-dessous. Un processeur a deux ''back-end'' séparés, un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par l'ALU entière, passent éventuellement dans l'unité mémoire, pour ensuite arriver à l'étape de ''writeback''. La FPU et le banc de registre flottant ont eux aussi leur propre ''back-end'' séparé.
[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]]
Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaqaue pipeline, à chaque cycle.
L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine.
Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants.
===La double émission entière===
Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait.
Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
==Les processeurs superscalaires dynamiques==
Les processeurs précédents, avec 2 pipelines, avaient une unité d'émission assez simple. Elle avait deux ports de décodage, et deux ports d'émission. Le nombre de ports de décodage et d'émission étaient identiques. Vous vous dites que c'est naturel, et intuitif, que c'est normal pour un processeur superscalaire. Mais en réalité, la majorité des processeurs superscalaires a plus de ports d'émission que de décodage. Expliquer pourquoi va cependant nous demander de parler de comment la superscalarité exploite les unités de calcul.
Posons-nous cette question : de combien d'unités un processeur superscalaire a besoin ? Les unités en question ne sont nécessairement des unité de calcul, l'unité mémoire et l'unité de branchement comptent aussi. La réponse dépend du nombre de voies. Si le ''front-end'' charge et décode un paquet de N instructions, exécuter ces N instructions simultanément demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section.
La réponse dépend aussi des contraintes d’appariement. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission parfaite devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement, ce qui ferait passer de 6 unités à 12. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, le cout en circuit encore plus prohibitif !
En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée, alors que la double émission parait plus adaptée.
Et c'est là que la différence entre ports d'émission et de décodage devient intéressante. L'idée est d'avoir une unité d'émission avec deux ports de décodage, et 4 port d'émission (un par unité). Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux micro-opérations tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu.
Les tout premiers CPU superscalaires étaient nombreux à utiliser cette technique. Les exemples classiques sont l'HyperSPARC, de l'alpha 21064 et du PowerPC 603. C'étaient des CPU double émission, avec une ALU entière, une unité mémoire et une FPU. Nous parlerons de '''partitionnement INT-FLOAT-MEM'''. Les contraintes d’appariement variaient grandement suivant le processeur.
Le PowerPC 603 implémentait une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur.
Avant de poursuivre, précisons que la FPU regroupe un additionneur flottant et un multiplieur flottant. Et ce fait peut être exploité pour approfondir le partitionnement. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions).
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple, facile à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. La raison est que le partitionnement permet d'émettre des paires, triplets, quadruplets de micro-opérations qui sont rares en pratique.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''.
Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, 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. Et enfin, il faut rajouter un port d'émission pour pouvoir émettre une micro-opération entière en plus.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle.
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
Reste à connecter les unités de calcul à l'unité d'émission (indirectement, car il y a des étages de pipeline entre les deux). Une implémentation basique utilisee un port d'émission par unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La présence d'un circuit multiplieur et d'un ''barrel shifter'' pose des problèmes pour le partitionnement. Il est possible d'avoir un port d'émission dédié au multiplieur, et un autre au ''barrel shifter'', qui sont séparés de ceux pour l'ALU entière. Il est alors possible d'émettre une opération entière, une multiplication et un décalage, simultanément. Nous parlerons de '''partitionnement entier''' pour désigner cette possibilité.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
Le partitionnement entier est cependant très rare, car le gain en performance serait trop faible au regard des couts. Quelques processeurs historiques l'utilisaient, mais ce n'est plus le cas de nos jours. La raison est que les multiplications et les décalages sont assez rares dans le code, ce qui fait que le port d'émission associés sont sous-utilisés. Et un port d'émission a un cout en circuit assez important. 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.
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul. L'exemple classique est justement lié aux circuits multiplieur et ''barrel shifters''. L'idée est que le multiplieur et le ''barrel shifter'' sont mis sur le même port d'émission qu'une ALU entière.
Prenons par exemple le processeur à double émission entière illustré ci-contre. Un port d'émission est relié à une ALU entière seule, et l'autre port d'émission alimente la seconde ALU entière et le multiplieur. La conséquence est que le processeur peut émettre deux opérations entières, dont une multiplication. Si on avait utilisé un port d'émission dédié au multiplieur, il aurait été possible d'émettre deux opérations entières et une multiplication. On perd donc une voie, mais on gagne en flexibilité et on économise des circuits.
[[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]]
En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais le partage des ports d'émission peut appliquer à n'importe quelle unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. 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.
La solution est de partager des ports d'émission, pour passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple relie une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires mélangent les trois techniques===
Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autre unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents.
* Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU.
* La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants.
* La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables.
* Le '''partage des ports d'émission''' réduit le cout en circuit du CPU, pour des pertes de performances mineures si bien fait.
De nos jours, le partitionnement est utilisé en complément de la duplication des ALU entières, de la duplication des autres unités, et du partage des ports d'émission. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. Le partage des ports d'émission a lui aussi mis un peu de temps pour s'imposer.
Prenons l'exemple des processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
==L'évolution historique de la superscalarité==
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. La plupart des CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 sont rapidement montés à de la quadruple émission, avec quelques exceptions. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 ||
|-
! 1995
| 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000
|-
! 1996
| class="f_bleu | 620 || || || || class="f_bleu | 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
===Les processeurs historiques à double émission===
Pour commencer, étudions le cas des premiers CPU superscalaires à double émission. Ils sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Notamment, la duplication des autres unités ne sert pour ainsi dire à rien, ce qui limite les possibilités, mais rendra les explications plus simples.
Il pouvaient se classer en trois types principaux, avec quelques processeurs qui ne rentraient pas dans ces cases. Le premier type utilise un ''partitionnement complet'' entre ALU, FPU et unité mémoire. Le second utilisait la double émission entière-flottante. Le troisième utilisait la double émission entière. Voici le tableau chronologique disant quel CPU utilisait quelle technique.
* Le partitionnement INT-FLOAT-MEM est en jaune.
* La double émission entière-flottante est en vert.
* La double émission entière est en rouge.
* Les CPU barrés utilisent la quadruple émission et seront vu dans la section suivante.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| class="f_vert | RS/6000 || || || class="f_rouge | i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| class="f_vert | RSC || class="f_vert | PA 7100 || SuperSPARC || || class="f_jaune | 21064 ||
|-
! 1993
| class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || ||
|-
! 1994
| <s>POWER 2</s> || PA 7200 || || || <s>21164</s> ||
|}
Les processeurs laissés de côté dans le tableau précédent ne rentrent pas dans les catégories précédentes.
Le PA 7200 est un PA-7100 auquel on aurait rajouté une ALU entière. Le processeur exécutait les multiplications entières dans la FPU, comme son prédécesseur. Les combinaisons autorisées étaient donc INT + INT, INT + MEM, INT + FLOAT/MUL, MEM + FLOAT/MUL.
Le SuperSPARC était un CPU triple émission, avec deux ALU entières, une FPU et une unité mémoire. Il avait de nombreuses contraintes d’appariement. Par exemple, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Une bonne partie de ces contraintes était lié au banc de registre, qui n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions. Il faut noter que la seconde ALU a été abandonnée lors du passage au CPU HyperSPARC, qui est de plus repassé en double émission, ce qui est une évolution à rebours de l'évolution des autres CPUs.
===Les processeurs historiques à quadruple émission===
C'est la même année , en 1994, que la quadruple émission est arrivée et que les CPU ont commencé à avoir de plus en plus d'unités de calcul. Même les designs restés en double émission avaient ajouté des unités de calcul. Il y avait alors trois stratégies pour la quadruple émission.
La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Les FPUs étaient soit dupliquées, soit scindées en deux en séparant l'additionneur du multiplieur flottant. Le POWER 2 dupliquait ses FPUs à la perfection, alors que l'21264 partitionnait la FPU. Les accès mémoire étaient traités comme des opérations entières, et le processeur pouvait : soit en faire un seul par cycle, soit en émettre deux, sous conditions (le cache devait être double port pour lire/écrire deux données à la fois, comme on le verra dans la suite du chapitre).
Les processeurs MIPS R8000 et R10000 allaient un peu plus loin, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités.
La seconde implémentation dupliquait l'ALU entière et combinait le tout avec un partitionnement INT-FLOAT-MEM. De quoi exécuter en même temps deux opérations entières, une opération flottante, et un accès mémoire. 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).
Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, avec au minimum deux ALU entières. Pour comprendre ce que je veux dire, prenons l'exemple de l'UltraSPARC. Il avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplier flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes. Le processeur pouvait émettre 4 instructions, tant qu'elles atterrissaient dans des unités différentes.
La même méthode peut être utilisée pour la double émission. Le Motorola 88110, sorti en 1991, était un CPU à double émission avec pas moins de 10 unités : deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise.
La morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes. Le tout est résumé dans le tableau ci-dessous, avec :
* en vert les CPU utilisant la première stratégie ;
* en rouge les CPU utilisant la seconde stratégie ;
* en jaune les CPU utilisant la troisième stratégie.
{|class="wikitable"
|+ Processeurs quadruple émission historiques
|-
! Année !! Processeur !! Unités
|-
! 1991
| class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques
|-
|-
! rowspan="2" | 1994
| class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'')
|-
| class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM
|-
! rowspan="3" | 1995
| class="f_jaune | PA 8000 || class="f_jaune | 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD
|-
| class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques
|-
| MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM
|-
! rowspan="2" | 1996
| class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM
|-
| class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL)
|}
===Les processeurs superscalaires modernes===
Les CPU superscalaires modernes ont des design légèrement différents des CPU historiques. Ils dupliquent l'unité entière, mais leur partitionnement est différent. En général, les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Par exemple, un CPU à 6 voies pourra émettre/exécuter 6 opérations entières simultanément, grâce à 6 ALU entières. Mais il s'agit là d'un maximum et certaines peuvent être remplacées par une opération flottante ou mémoire, qui utilisent l'unité mémoire et/ou la FPU.
Un exemple assez parlant est les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon. C'était une microarchitecture à triple émission, avec trois ALU entières et trois unités mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements.
Il s'agit là d'une tendance, cependant. Les CPU superscalaires tendent à mettre le paquet niveau opérations arithmétiques, mais pas forcément au point de remplir la totalité de la largeur du processeur. La règle est que pour N voies, le processeur dispose d'au moins N/2 ALU entières. Attention, cela ne signifie pas que le processeur émet systématiquement N/2 opération entières. Il peut par exemple émettre une combinaison d'opérations flottantes et mémoire, avec des branchements. Et N/2 est une borne basse, la plupart des CPU x86 ont environ tournent plutôt autour des 2/3, voire des 3/4.
En général, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. La conséquence est que les unités mémoire sont dupliquées, en seconde place dans l'ordre d'importance. Les processeurs x86 ont commencé à dupliquer leurs unités mémoire dès qu'ils ont atteint la quadruple émission, pour les CPU grand public. La microarchitecture Core d'Intel utilisait la quadruple émission, avec deux unités mémoire et trois ALU entières. Les microarchitectures d'AMD K5 et K6 utilisaient aussi la quadruple émission et avaient deux ALUs pour deux unités mémoire.
Plus récemment, la microarchitecture ''Golden Cove'' dispose de cinq unités de calcul trois unités mémoire (deux spécialisées dans les écritures, trois dans les lectures). La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire.Il y a donc soit égalité, soit avantage pour les ALU entières.
Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les CPU superscalaire à exécution dans le désordre==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Les unités d'exécution dans le désordre===
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de 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===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Avec plusieurs dizaines d'unités de calcul différentes, ajouter autant 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.
La première solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Le processeur détecte quand il n'y a pas assez de ports pour servir toutes les micro-opérations. Quand ça arrive, l'unité d'émission émet assez de micro-opérations pour exploiter les ports disponibles, mais met en attente les micro-opérations en trop, le temps que les ports se libèrent. Pour cela, le banc de registre est géré par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Une autre solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une dernière solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
rd30tycsi11fathnl09g22xpje708li
773180
773179
2026-09-25T20:20:30Z
Mewtow
31375
/* Les unités d'exécution dans le désordre */
773180
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===L'impact des contraintes d’appariement sur la performance===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour la suite du chapitre===
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==Le ''front-end'' des processeurs superscalaires==
Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple.
Intuitivement, charger, décoder et 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 certaines processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. 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
|}
Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs.
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===L'émission parallèle des instructions===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elle sont propagée au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage).
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
==Les processeurs implémentés avec plusieurs pipelines==
Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Vous vous dites que l'implémentation d'un processeur superscalaire n'est pas la même suivant que le CPU utilise l'exécution dans le désordre, ou non. Mais dans les faits, la distinction est plus subtile que ca. Pour l'expliquer, je vais utiliser une classification originale, qui sépare les processeurs superscalaires en trois sous-types. La distinction se fait sur la manière dont la superscalarité est implémentée en matériel.La classification que j'utilise distingue :
* Les processeurs avec plusieurs pipelines, sans exécution dans le désordre.
* Les processeurs superscalaires avec un pipeline dynamique.
* Les processeurs avec des fenêtres d'instruction décentralisées.
La distinction n'est pas qu'une question d'implémentation matérielle. Elle impacte aussi le ratio entre instructions chargées/décodées et instructions émises. Vous vous dites que les deux sont égales, mais ce n'est le cas que sur les processeurs avec N pipelines ! Sur les autres processeurs, il est possible de décoder 4 instructions, mais d'en émettre maximum 6 par cycle ! Et sur les CPU avec des fenêtres d'instruction décentralisées, il y a aussi une différence entre le nombre d'instructions qui sont ''dispatch'' et ''schedule''.
La catégorie la plus simple est celle des '''processeurs avec plusieurs pipelines'''. Ce sont des processeurs superscalaires sans exécution dans le désordre. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction.
Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, d'une FPU, éventuellement d'un second banc de registres, guère plus. Il permet d'exécuter une seconde opération de calcul, pas plus, ce qui est à l'origine de contraintes d'appariemment assez fortes. De plus, il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section.
===La double émission entière-flottante===
L'exemple le plus simple utilise un pipeline pour les opérations flottantes, séparé du pipeline originel. Pour bien expliquer les choses, prenons le processeur illustré ci-dessous. Un processeur a deux ''back-end'' séparés, un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par l'ALU entière, passent éventuellement dans l'unité mémoire, pour ensuite arriver à l'étape de ''writeback''. La FPU et le banc de registre flottant ont eux aussi leur propre ''back-end'' séparé.
[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]]
Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaqaue pipeline, à chaque cycle.
L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine.
Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants.
===La double émission entière===
Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait.
Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
==Les processeurs superscalaires dynamiques==
Les processeurs précédents, avec 2 pipelines, avaient une unité d'émission assez simple. Elle avait deux ports de décodage, et deux ports d'émission. Le nombre de ports de décodage et d'émission étaient identiques. Vous vous dites que c'est naturel, et intuitif, que c'est normal pour un processeur superscalaire. Mais en réalité, la majorité des processeurs superscalaires a plus de ports d'émission que de décodage. Expliquer pourquoi va cependant nous demander de parler de comment la superscalarité exploite les unités de calcul.
Posons-nous cette question : de combien d'unités un processeur superscalaire a besoin ? Les unités en question ne sont nécessairement des unité de calcul, l'unité mémoire et l'unité de branchement comptent aussi. La réponse dépend du nombre de voies. Si le ''front-end'' charge et décode un paquet de N instructions, exécuter ces N instructions simultanément demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section.
La réponse dépend aussi des contraintes d’appariement. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission parfaite devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement, ce qui ferait passer de 6 unités à 12. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, le cout en circuit encore plus prohibitif !
En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée, alors que la double émission parait plus adaptée.
Et c'est là que la différence entre ports d'émission et de décodage devient intéressante. L'idée est d'avoir une unité d'émission avec deux ports de décodage, et 4 port d'émission (un par unité). Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux micro-opérations tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu.
Les tout premiers CPU superscalaires étaient nombreux à utiliser cette technique. Les exemples classiques sont l'HyperSPARC, de l'alpha 21064 et du PowerPC 603. C'étaient des CPU double émission, avec une ALU entière, une unité mémoire et une FPU. Nous parlerons de '''partitionnement INT-FLOAT-MEM'''. Les contraintes d’appariement variaient grandement suivant le processeur.
Le PowerPC 603 implémentait une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur.
Avant de poursuivre, précisons que la FPU regroupe un additionneur flottant et un multiplieur flottant. Et ce fait peut être exploité pour approfondir le partitionnement. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions).
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple, facile à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. La raison est que le partitionnement permet d'émettre des paires, triplets, quadruplets de micro-opérations qui sont rares en pratique.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''.
Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, 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. Et enfin, il faut rajouter un port d'émission pour pouvoir émettre une micro-opération entière en plus.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle.
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
Reste à connecter les unités de calcul à l'unité d'émission (indirectement, car il y a des étages de pipeline entre les deux). Une implémentation basique utilisee un port d'émission par unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La présence d'un circuit multiplieur et d'un ''barrel shifter'' pose des problèmes pour le partitionnement. Il est possible d'avoir un port d'émission dédié au multiplieur, et un autre au ''barrel shifter'', qui sont séparés de ceux pour l'ALU entière. Il est alors possible d'émettre une opération entière, une multiplication et un décalage, simultanément. Nous parlerons de '''partitionnement entier''' pour désigner cette possibilité.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
Le partitionnement entier est cependant très rare, car le gain en performance serait trop faible au regard des couts. Quelques processeurs historiques l'utilisaient, mais ce n'est plus le cas de nos jours. La raison est que les multiplications et les décalages sont assez rares dans le code, ce qui fait que le port d'émission associés sont sous-utilisés. Et un port d'émission a un cout en circuit assez important. 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.
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul. L'exemple classique est justement lié aux circuits multiplieur et ''barrel shifters''. L'idée est que le multiplieur et le ''barrel shifter'' sont mis sur le même port d'émission qu'une ALU entière.
Prenons par exemple le processeur à double émission entière illustré ci-contre. Un port d'émission est relié à une ALU entière seule, et l'autre port d'émission alimente la seconde ALU entière et le multiplieur. La conséquence est que le processeur peut émettre deux opérations entières, dont une multiplication. Si on avait utilisé un port d'émission dédié au multiplieur, il aurait été possible d'émettre deux opérations entières et une multiplication. On perd donc une voie, mais on gagne en flexibilité et on économise des circuits.
[[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]]
En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais le partage des ports d'émission peut appliquer à n'importe quelle unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. 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.
La solution est de partager des ports d'émission, pour passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple relie une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires mélangent les trois techniques===
Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autre unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents.
* Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU.
* La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants.
* La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables.
* Le '''partage des ports d'émission''' réduit le cout en circuit du CPU, pour des pertes de performances mineures si bien fait.
De nos jours, le partitionnement est utilisé en complément de la duplication des ALU entières, de la duplication des autres unités, et du partage des ports d'émission. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. Le partage des ports d'émission a lui aussi mis un peu de temps pour s'imposer.
Prenons l'exemple des processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
==L'évolution historique de la superscalarité==
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. La plupart des CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 sont rapidement montés à de la quadruple émission, avec quelques exceptions. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 ||
|-
! 1995
| 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000
|-
! 1996
| class="f_bleu | 620 || || || || class="f_bleu | 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
===Les processeurs historiques à double émission===
Pour commencer, étudions le cas des premiers CPU superscalaires à double émission. Ils sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Notamment, la duplication des autres unités ne sert pour ainsi dire à rien, ce qui limite les possibilités, mais rendra les explications plus simples.
Il pouvaient se classer en trois types principaux, avec quelques processeurs qui ne rentraient pas dans ces cases. Le premier type utilise un ''partitionnement complet'' entre ALU, FPU et unité mémoire. Le second utilisait la double émission entière-flottante. Le troisième utilisait la double émission entière. Voici le tableau chronologique disant quel CPU utilisait quelle technique.
* Le partitionnement INT-FLOAT-MEM est en jaune.
* La double émission entière-flottante est en vert.
* La double émission entière est en rouge.
* Les CPU barrés utilisent la quadruple émission et seront vu dans la section suivante.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| class="f_vert | RS/6000 || || || class="f_rouge | i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| class="f_vert | RSC || class="f_vert | PA 7100 || SuperSPARC || || class="f_jaune | 21064 ||
|-
! 1993
| class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || ||
|-
! 1994
| <s>POWER 2</s> || PA 7200 || || || <s>21164</s> ||
|}
Les processeurs laissés de côté dans le tableau précédent ne rentrent pas dans les catégories précédentes.
Le PA 7200 est un PA-7100 auquel on aurait rajouté une ALU entière. Le processeur exécutait les multiplications entières dans la FPU, comme son prédécesseur. Les combinaisons autorisées étaient donc INT + INT, INT + MEM, INT + FLOAT/MUL, MEM + FLOAT/MUL.
Le SuperSPARC était un CPU triple émission, avec deux ALU entières, une FPU et une unité mémoire. Il avait de nombreuses contraintes d’appariement. Par exemple, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Une bonne partie de ces contraintes était lié au banc de registre, qui n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions. Il faut noter que la seconde ALU a été abandonnée lors du passage au CPU HyperSPARC, qui est de plus repassé en double émission, ce qui est une évolution à rebours de l'évolution des autres CPUs.
===Les processeurs historiques à quadruple émission===
C'est la même année , en 1994, que la quadruple émission est arrivée et que les CPU ont commencé à avoir de plus en plus d'unités de calcul. Même les designs restés en double émission avaient ajouté des unités de calcul. Il y avait alors trois stratégies pour la quadruple émission.
La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Les FPUs étaient soit dupliquées, soit scindées en deux en séparant l'additionneur du multiplieur flottant. Le POWER 2 dupliquait ses FPUs à la perfection, alors que l'21264 partitionnait la FPU. Les accès mémoire étaient traités comme des opérations entières, et le processeur pouvait : soit en faire un seul par cycle, soit en émettre deux, sous conditions (le cache devait être double port pour lire/écrire deux données à la fois, comme on le verra dans la suite du chapitre).
Les processeurs MIPS R8000 et R10000 allaient un peu plus loin, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités.
La seconde implémentation dupliquait l'ALU entière et combinait le tout avec un partitionnement INT-FLOAT-MEM. De quoi exécuter en même temps deux opérations entières, une opération flottante, et un accès mémoire. 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).
Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, avec au minimum deux ALU entières. Pour comprendre ce que je veux dire, prenons l'exemple de l'UltraSPARC. Il avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplier flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes. Le processeur pouvait émettre 4 instructions, tant qu'elles atterrissaient dans des unités différentes.
La même méthode peut être utilisée pour la double émission. Le Motorola 88110, sorti en 1991, était un CPU à double émission avec pas moins de 10 unités : deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise.
La morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes. Le tout est résumé dans le tableau ci-dessous, avec :
* en vert les CPU utilisant la première stratégie ;
* en rouge les CPU utilisant la seconde stratégie ;
* en jaune les CPU utilisant la troisième stratégie.
{|class="wikitable"
|+ Processeurs quadruple émission historiques
|-
! Année !! Processeur !! Unités
|-
! 1991
| class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques
|-
|-
! rowspan="2" | 1994
| class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'')
|-
| class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM
|-
! rowspan="3" | 1995
| class="f_jaune | PA 8000 || class="f_jaune | 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD
|-
| class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques
|-
| MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM
|-
! rowspan="2" | 1996
| class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM
|-
| class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL)
|}
===Les processeurs superscalaires modernes===
Les CPU superscalaires modernes ont des design légèrement différents des CPU historiques. Ils dupliquent l'unité entière, mais leur partitionnement est différent. En général, les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Par exemple, un CPU à 6 voies pourra émettre/exécuter 6 opérations entières simultanément, grâce à 6 ALU entières. Mais il s'agit là d'un maximum et certaines peuvent être remplacées par une opération flottante ou mémoire, qui utilisent l'unité mémoire et/ou la FPU.
Un exemple assez parlant est les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon. C'était une microarchitecture à triple émission, avec trois ALU entières et trois unités mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements.
Il s'agit là d'une tendance, cependant. Les CPU superscalaires tendent à mettre le paquet niveau opérations arithmétiques, mais pas forcément au point de remplir la totalité de la largeur du processeur. La règle est que pour N voies, le processeur dispose d'au moins N/2 ALU entières. Attention, cela ne signifie pas que le processeur émet systématiquement N/2 opération entières. Il peut par exemple émettre une combinaison d'opérations flottantes et mémoire, avec des branchements. Et N/2 est une borne basse, la plupart des CPU x86 ont environ tournent plutôt autour des 2/3, voire des 3/4.
En général, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. La conséquence est que les unités mémoire sont dupliquées, en seconde place dans l'ordre d'importance. Les processeurs x86 ont commencé à dupliquer leurs unités mémoire dès qu'ils ont atteint la quadruple émission, pour les CPU grand public. La microarchitecture Core d'Intel utilisait la quadruple émission, avec deux unités mémoire et trois ALU entières. Les microarchitectures d'AMD K5 et K6 utilisaient aussi la quadruple émission et avaient deux ALUs pour deux unités mémoire.
Plus récemment, la microarchitecture ''Golden Cove'' dispose de cinq unités de calcul trois unités mémoire (deux spécialisées dans les écritures, trois dans les lectures). La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire.Il y a donc soit égalité, soit avantage pour les ALU entières.
Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les CPU superscalaire à exécution dans le désordre==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Les unités d'exécution dans le désordre===
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
===L'imapct sur le banc de registres===
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions auto-aligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions auto-aligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez longs, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Le banc de registre des processeurs superscalaires larges===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Avec plusieurs dizaines d'unités de calcul différentes, ajouter autant 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.
La première solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Le processeur détecte quand il n'y a pas assez de ports pour servir toutes les micro-opérations. Quand ça arrive, l'unité d'émission émet assez de micro-opérations pour exploiter les ports disponibles, mais met en attente les micro-opérations en trop, le temps que les ports se libèrent. Pour cela, le banc de registre est géré par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Une autre solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une dernière solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
tvdw8ympy9mh3up810ogaz8wm0cxuir
773181
773180
2026-09-25T20:21:18Z
Mewtow
31375
/* L'imapct sur le banc de registres */
773181
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===L'impact des contraintes d’appariement sur la performance===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour la suite du chapitre===
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==Le ''front-end'' des processeurs superscalaires==
Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple.
Intuitivement, charger, décoder et 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 certaines processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. 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
|}
Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs.
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===L'émission parallèle des instructions===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elle sont propagée au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage).
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
==Les processeurs implémentés avec plusieurs pipelines==
Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Vous vous dites que l'implémentation d'un processeur superscalaire n'est pas la même suivant que le CPU utilise l'exécution dans le désordre, ou non. Mais dans les faits, la distinction est plus subtile que ca. Pour l'expliquer, je vais utiliser une classification originale, qui sépare les processeurs superscalaires en trois sous-types. La distinction se fait sur la manière dont la superscalarité est implémentée en matériel.La classification que j'utilise distingue :
* Les processeurs avec plusieurs pipelines, sans exécution dans le désordre.
* Les processeurs superscalaires avec un pipeline dynamique.
* Les processeurs avec des fenêtres d'instruction décentralisées.
La distinction n'est pas qu'une question d'implémentation matérielle. Elle impacte aussi le ratio entre instructions chargées/décodées et instructions émises. Vous vous dites que les deux sont égales, mais ce n'est le cas que sur les processeurs avec N pipelines ! Sur les autres processeurs, il est possible de décoder 4 instructions, mais d'en émettre maximum 6 par cycle ! Et sur les CPU avec des fenêtres d'instruction décentralisées, il y a aussi une différence entre le nombre d'instructions qui sont ''dispatch'' et ''schedule''.
La catégorie la plus simple est celle des '''processeurs avec plusieurs pipelines'''. Ce sont des processeurs superscalaires sans exécution dans le désordre. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction.
Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, d'une FPU, éventuellement d'un second banc de registres, guère plus. Il permet d'exécuter une seconde opération de calcul, pas plus, ce qui est à l'origine de contraintes d'appariemment assez fortes. De plus, il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section.
===La double émission entière-flottante===
L'exemple le plus simple utilise un pipeline pour les opérations flottantes, séparé du pipeline originel. Pour bien expliquer les choses, prenons le processeur illustré ci-dessous. Un processeur a deux ''back-end'' séparés, un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par l'ALU entière, passent éventuellement dans l'unité mémoire, pour ensuite arriver à l'étape de ''writeback''. La FPU et le banc de registre flottant ont eux aussi leur propre ''back-end'' séparé.
[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]]
Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaqaue pipeline, à chaque cycle.
L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine.
Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants.
===La double émission entière===
Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait.
Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
==Les processeurs superscalaires dynamiques==
Les processeurs précédents, avec 2 pipelines, avaient une unité d'émission assez simple. Elle avait deux ports de décodage, et deux ports d'émission. Le nombre de ports de décodage et d'émission étaient identiques. Vous vous dites que c'est naturel, et intuitif, que c'est normal pour un processeur superscalaire. Mais en réalité, la majorité des processeurs superscalaires a plus de ports d'émission que de décodage. Expliquer pourquoi va cependant nous demander de parler de comment la superscalarité exploite les unités de calcul.
Posons-nous cette question : de combien d'unités un processeur superscalaire a besoin ? Les unités en question ne sont nécessairement des unité de calcul, l'unité mémoire et l'unité de branchement comptent aussi. La réponse dépend du nombre de voies. Si le ''front-end'' charge et décode un paquet de N instructions, exécuter ces N instructions simultanément demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section.
La réponse dépend aussi des contraintes d’appariement. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission parfaite devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement, ce qui ferait passer de 6 unités à 12. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, le cout en circuit encore plus prohibitif !
En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée, alors que la double émission parait plus adaptée.
Et c'est là que la différence entre ports d'émission et de décodage devient intéressante. L'idée est d'avoir une unité d'émission avec deux ports de décodage, et 4 port d'émission (un par unité). Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux micro-opérations tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu.
Les tout premiers CPU superscalaires étaient nombreux à utiliser cette technique. Les exemples classiques sont l'HyperSPARC, de l'alpha 21064 et du PowerPC 603. C'étaient des CPU double émission, avec une ALU entière, une unité mémoire et une FPU. Nous parlerons de '''partitionnement INT-FLOAT-MEM'''. Les contraintes d’appariement variaient grandement suivant le processeur.
Le PowerPC 603 implémentait une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur.
Avant de poursuivre, précisons que la FPU regroupe un additionneur flottant et un multiplieur flottant. Et ce fait peut être exploité pour approfondir le partitionnement. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions).
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple, facile à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. La raison est que le partitionnement permet d'émettre des paires, triplets, quadruplets de micro-opérations qui sont rares en pratique.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''.
Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, 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. Et enfin, il faut rajouter un port d'émission pour pouvoir émettre une micro-opération entière en plus.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle.
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
Reste à connecter les unités de calcul à l'unité d'émission (indirectement, car il y a des étages de pipeline entre les deux). Une implémentation basique utilisee un port d'émission par unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La présence d'un circuit multiplieur et d'un ''barrel shifter'' pose des problèmes pour le partitionnement. Il est possible d'avoir un port d'émission dédié au multiplieur, et un autre au ''barrel shifter'', qui sont séparés de ceux pour l'ALU entière. Il est alors possible d'émettre une opération entière, une multiplication et un décalage, simultanément. Nous parlerons de '''partitionnement entier''' pour désigner cette possibilité.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
Le partitionnement entier est cependant très rare, car le gain en performance serait trop faible au regard des couts. Quelques processeurs historiques l'utilisaient, mais ce n'est plus le cas de nos jours. La raison est que les multiplications et les décalages sont assez rares dans le code, ce qui fait que le port d'émission associés sont sous-utilisés. Et un port d'émission a un cout en circuit assez important. 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.
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul. L'exemple classique est justement lié aux circuits multiplieur et ''barrel shifters''. L'idée est que le multiplieur et le ''barrel shifter'' sont mis sur le même port d'émission qu'une ALU entière.
Prenons par exemple le processeur à double émission entière illustré ci-contre. Un port d'émission est relié à une ALU entière seule, et l'autre port d'émission alimente la seconde ALU entière et le multiplieur. La conséquence est que le processeur peut émettre deux opérations entières, dont une multiplication. Si on avait utilisé un port d'émission dédié au multiplieur, il aurait été possible d'émettre deux opérations entières et une multiplication. On perd donc une voie, mais on gagne en flexibilité et on économise des circuits.
[[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]]
En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais le partage des ports d'émission peut appliquer à n'importe quelle unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. 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.
La solution est de partager des ports d'émission, pour passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple relie une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires mélangent les trois techniques===
Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autre unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents.
* Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU.
* La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants.
* La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables.
* Le '''partage des ports d'émission''' réduit le cout en circuit du CPU, pour des pertes de performances mineures si bien fait.
De nos jours, le partitionnement est utilisé en complément de la duplication des ALU entières, de la duplication des autres unités, et du partage des ports d'émission. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. Le partage des ports d'émission a lui aussi mis un peu de temps pour s'imposer.
Prenons l'exemple des processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
==L'évolution historique de la superscalarité==
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. La plupart des CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 sont rapidement montés à de la quadruple émission, avec quelques exceptions. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 ||
|-
! 1995
| 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000
|-
! 1996
| class="f_bleu | 620 || || || || class="f_bleu | 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
===Les processeurs historiques à double émission===
Pour commencer, étudions le cas des premiers CPU superscalaires à double émission. Ils sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Notamment, la duplication des autres unités ne sert pour ainsi dire à rien, ce qui limite les possibilités, mais rendra les explications plus simples.
Il pouvaient se classer en trois types principaux, avec quelques processeurs qui ne rentraient pas dans ces cases. Le premier type utilise un ''partitionnement complet'' entre ALU, FPU et unité mémoire. Le second utilisait la double émission entière-flottante. Le troisième utilisait la double émission entière. Voici le tableau chronologique disant quel CPU utilisait quelle technique.
* Le partitionnement INT-FLOAT-MEM est en jaune.
* La double émission entière-flottante est en vert.
* La double émission entière est en rouge.
* Les CPU barrés utilisent la quadruple émission et seront vu dans la section suivante.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| class="f_vert | RS/6000 || || || class="f_rouge | i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| class="f_vert | RSC || class="f_vert | PA 7100 || SuperSPARC || || class="f_jaune | 21064 ||
|-
! 1993
| class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || ||
|-
! 1994
| <s>POWER 2</s> || PA 7200 || || || <s>21164</s> ||
|}
Les processeurs laissés de côté dans le tableau précédent ne rentrent pas dans les catégories précédentes.
Le PA 7200 est un PA-7100 auquel on aurait rajouté une ALU entière. Le processeur exécutait les multiplications entières dans la FPU, comme son prédécesseur. Les combinaisons autorisées étaient donc INT + INT, INT + MEM, INT + FLOAT/MUL, MEM + FLOAT/MUL.
Le SuperSPARC était un CPU triple émission, avec deux ALU entières, une FPU et une unité mémoire. Il avait de nombreuses contraintes d’appariement. Par exemple, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Une bonne partie de ces contraintes était lié au banc de registre, qui n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions. Il faut noter que la seconde ALU a été abandonnée lors du passage au CPU HyperSPARC, qui est de plus repassé en double émission, ce qui est une évolution à rebours de l'évolution des autres CPUs.
===Les processeurs historiques à quadruple émission===
C'est la même année , en 1994, que la quadruple émission est arrivée et que les CPU ont commencé à avoir de plus en plus d'unités de calcul. Même les designs restés en double émission avaient ajouté des unités de calcul. Il y avait alors trois stratégies pour la quadruple émission.
La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Les FPUs étaient soit dupliquées, soit scindées en deux en séparant l'additionneur du multiplieur flottant. Le POWER 2 dupliquait ses FPUs à la perfection, alors que l'21264 partitionnait la FPU. Les accès mémoire étaient traités comme des opérations entières, et le processeur pouvait : soit en faire un seul par cycle, soit en émettre deux, sous conditions (le cache devait être double port pour lire/écrire deux données à la fois, comme on le verra dans la suite du chapitre).
Les processeurs MIPS R8000 et R10000 allaient un peu plus loin, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités.
La seconde implémentation dupliquait l'ALU entière et combinait le tout avec un partitionnement INT-FLOAT-MEM. De quoi exécuter en même temps deux opérations entières, une opération flottante, et un accès mémoire. 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).
Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, avec au minimum deux ALU entières. Pour comprendre ce que je veux dire, prenons l'exemple de l'UltraSPARC. Il avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplier flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes. Le processeur pouvait émettre 4 instructions, tant qu'elles atterrissaient dans des unités différentes.
La même méthode peut être utilisée pour la double émission. Le Motorola 88110, sorti en 1991, était un CPU à double émission avec pas moins de 10 unités : deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise.
La morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes. Le tout est résumé dans le tableau ci-dessous, avec :
* en vert les CPU utilisant la première stratégie ;
* en rouge les CPU utilisant la seconde stratégie ;
* en jaune les CPU utilisant la troisième stratégie.
{|class="wikitable"
|+ Processeurs quadruple émission historiques
|-
! Année !! Processeur !! Unités
|-
! 1991
| class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques
|-
|-
! rowspan="2" | 1994
| class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'')
|-
| class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM
|-
! rowspan="3" | 1995
| class="f_jaune | PA 8000 || class="f_jaune | 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD
|-
| class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques
|-
| MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM
|-
! rowspan="2" | 1996
| class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM
|-
| class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL)
|}
===Les processeurs superscalaires modernes===
Les CPU superscalaires modernes ont des design légèrement différents des CPU historiques. Ils dupliquent l'unité entière, mais leur partitionnement est différent. En général, les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Par exemple, un CPU à 6 voies pourra émettre/exécuter 6 opérations entières simultanément, grâce à 6 ALU entières. Mais il s'agit là d'un maximum et certaines peuvent être remplacées par une opération flottante ou mémoire, qui utilisent l'unité mémoire et/ou la FPU.
Un exemple assez parlant est les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon. C'était une microarchitecture à triple émission, avec trois ALU entières et trois unités mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements.
Il s'agit là d'une tendance, cependant. Les CPU superscalaires tendent à mettre le paquet niveau opérations arithmétiques, mais pas forcément au point de remplir la totalité de la largeur du processeur. La règle est que pour N voies, le processeur dispose d'au moins N/2 ALU entières. Attention, cela ne signifie pas que le processeur émet systématiquement N/2 opération entières. Il peut par exemple émettre une combinaison d'opérations flottantes et mémoire, avec des branchements. Et N/2 est une borne basse, la plupart des CPU x86 ont environ tournent plutôt autour des 2/3, voire des 3/4.
En général, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. La conséquence est que les unités mémoire sont dupliquées, en seconde place dans l'ordre d'importance. Les processeurs x86 ont commencé à dupliquer leurs unités mémoire dès qu'ils ont atteint la quadruple émission, pour les CPU grand public. La microarchitecture Core d'Intel utilisait la quadruple émission, avec deux unités mémoire et trois ALU entières. Les microarchitectures d'AMD K5 et K6 utilisaient aussi la quadruple émission et avaient deux ALUs pour deux unités mémoire.
Plus récemment, la microarchitecture ''Golden Cove'' dispose de cinq unités de calcul trois unités mémoire (deux spécialisées dans les écritures, trois dans les lectures). La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire.Il y a donc soit égalité, soit avantage pour les ALU entières.
Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les CPU superscalaire à exécution dans le désordre==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Les unités d'exécution dans le désordre===
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports de ''dispatch''. Mais avec des stations de réservation, il faut le double des ports de ''schedule''. La raison est que les registres sont lus après émission avec une fenê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 de ''schedule'' que de ''dispatch''.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de 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===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Avec plusieurs dizaines d'unités de calcul différentes, ajouter autant 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.
La première solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Le processeur détecte quand il n'y a pas assez de ports pour servir toutes les micro-opérations. Quand ça arrive, l'unité d'émission émet assez de micro-opérations pour exploiter les ports disponibles, mais met en attente les micro-opérations en trop, le temps que les ports se libèrent. Pour cela, le banc de registre est géré par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Une autre solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une dernière solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
h1u1zxftmdhzc0tsi5cvtckq9qo4b90
773182
773181
2026-09-25T20:26:50Z
Mewtow
31375
/* L'évolution historique de la superscalarité */
773182
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===L'impact des contraintes d’appariement sur la performance===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour la suite du chapitre===
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==Le ''front-end'' des processeurs superscalaires==
Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple.
Intuitivement, charger, décoder et 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 certaines processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. 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
|}
Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs.
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===L'émission parallèle des instructions===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elle sont propagée au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage).
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
==Les processeurs implémentés avec plusieurs pipelines==
Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Vous vous dites que l'implémentation d'un processeur superscalaire n'est pas la même suivant que le CPU utilise l'exécution dans le désordre, ou non. Mais dans les faits, la distinction est plus subtile que ca. Pour l'expliquer, je vais utiliser une classification originale, qui sépare les processeurs superscalaires en trois sous-types. La distinction se fait sur la manière dont la superscalarité est implémentée en matériel.La classification que j'utilise distingue :
* Les processeurs avec plusieurs pipelines, sans exécution dans le désordre.
* Les processeurs superscalaires avec un pipeline dynamique.
* Les processeurs avec des fenêtres d'instruction décentralisées.
La distinction n'est pas qu'une question d'implémentation matérielle. Elle impacte aussi le ratio entre instructions chargées/décodées et instructions émises. Vous vous dites que les deux sont égales, mais ce n'est le cas que sur les processeurs avec N pipelines ! Sur les autres processeurs, il est possible de décoder 4 instructions, mais d'en émettre maximum 6 par cycle ! Et sur les CPU avec des fenêtres d'instruction décentralisées, il y a aussi une différence entre le nombre d'instructions qui sont ''dispatch'' et ''schedule''.
La catégorie la plus simple est celle des '''processeurs avec plusieurs pipelines'''. Ce sont des processeurs superscalaires sans exécution dans le désordre. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction.
Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, d'une FPU, éventuellement d'un second banc de registres, guère plus. Il permet d'exécuter une seconde opération de calcul, pas plus, ce qui est à l'origine de contraintes d'appariemment assez fortes. De plus, il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section.
===La double émission entière-flottante===
L'exemple le plus simple utilise un pipeline pour les opérations flottantes, séparé du pipeline originel. Pour bien expliquer les choses, prenons le processeur illustré ci-dessous. Un processeur a deux ''back-end'' séparés, un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par l'ALU entière, passent éventuellement dans l'unité mémoire, pour ensuite arriver à l'étape de ''writeback''. La FPU et le banc de registre flottant ont eux aussi leur propre ''back-end'' séparé.
[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]]
Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaqaue pipeline, à chaque cycle.
L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine.
Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants.
===La double émission entière===
Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait.
Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
==Les processeurs superscalaires dynamiques==
Les processeurs précédents, avec 2 pipelines, avaient une unité d'émission assez simple. Elle avait deux ports de décodage, et deux ports d'émission. Le nombre de ports de décodage et d'émission étaient identiques. Vous vous dites que c'est naturel, et intuitif, que c'est normal pour un processeur superscalaire. Mais en réalité, la majorité des processeurs superscalaires a plus de ports d'émission que de décodage. Expliquer pourquoi va cependant nous demander de parler de comment la superscalarité exploite les unités de calcul.
Posons-nous cette question : de combien d'unités un processeur superscalaire a besoin ? Les unités en question ne sont nécessairement des unité de calcul, l'unité mémoire et l'unité de branchement comptent aussi. La réponse dépend du nombre de voies. Si le ''front-end'' charge et décode un paquet de N instructions, exécuter ces N instructions simultanément demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section.
La réponse dépend aussi des contraintes d’appariement. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission parfaite devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement, ce qui ferait passer de 6 unités à 12. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, le cout en circuit encore plus prohibitif !
En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée, alors que la double émission parait plus adaptée.
Et c'est là que la différence entre ports d'émission et de décodage devient intéressante. L'idée est d'avoir une unité d'émission avec deux ports de décodage, et 4 port d'émission (un par unité). Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux micro-opérations tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu.
Les tout premiers CPU superscalaires étaient nombreux à utiliser cette technique. Les exemples classiques sont l'HyperSPARC, de l'alpha 21064 et du PowerPC 603. C'étaient des CPU double émission, avec une ALU entière, une unité mémoire et une FPU. Nous parlerons de '''partitionnement INT-FLOAT-MEM'''. Les contraintes d’appariement variaient grandement suivant le processeur.
Le PowerPC 603 implémentait une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur.
Avant de poursuivre, précisons que la FPU regroupe un additionneur flottant et un multiplieur flottant. Et ce fait peut être exploité pour approfondir le partitionnement. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions).
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple, facile à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. La raison est que le partitionnement permet d'émettre des paires, triplets, quadruplets de micro-opérations qui sont rares en pratique.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''.
Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, 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. Et enfin, il faut rajouter un port d'émission pour pouvoir émettre une micro-opération entière en plus.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle.
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
Reste à connecter les unités de calcul à l'unité d'émission (indirectement, car il y a des étages de pipeline entre les deux). Une implémentation basique utilisee un port d'émission par unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La présence d'un circuit multiplieur et d'un ''barrel shifter'' pose des problèmes pour le partitionnement. Il est possible d'avoir un port d'émission dédié au multiplieur, et un autre au ''barrel shifter'', qui sont séparés de ceux pour l'ALU entière. Il est alors possible d'émettre une opération entière, une multiplication et un décalage, simultanément. Nous parlerons de '''partitionnement entier''' pour désigner cette possibilité.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
Le partitionnement entier est cependant très rare, car le gain en performance serait trop faible au regard des couts. Quelques processeurs historiques l'utilisaient, mais ce n'est plus le cas de nos jours. La raison est que les multiplications et les décalages sont assez rares dans le code, ce qui fait que le port d'émission associés sont sous-utilisés. Et un port d'émission a un cout en circuit assez important. 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.
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul. L'exemple classique est justement lié aux circuits multiplieur et ''barrel shifters''. L'idée est que le multiplieur et le ''barrel shifter'' sont mis sur le même port d'émission qu'une ALU entière.
Prenons par exemple le processeur à double émission entière illustré ci-contre. Un port d'émission est relié à une ALU entière seule, et l'autre port d'émission alimente la seconde ALU entière et le multiplieur. La conséquence est que le processeur peut émettre deux opérations entières, dont une multiplication. Si on avait utilisé un port d'émission dédié au multiplieur, il aurait été possible d'émettre deux opérations entières et une multiplication. On perd donc une voie, mais on gagne en flexibilité et on économise des circuits.
[[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]]
En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais le partage des ports d'émission peut appliquer à n'importe quelle unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. 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.
La solution est de partager des ports d'émission, pour passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple relie une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires mélangent les trois techniques===
Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autre unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents.
* Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU.
* La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants.
* La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables.
* Le '''partage des ports d'émission''' réduit le cout en circuit du CPU, pour des pertes de performances mineures si bien fait.
De nos jours, le partitionnement est utilisé en complément de la duplication des ALU entières, de la duplication des autres unités, et du partage des ports d'émission. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. Le partage des ports d'émission a lui aussi mis un peu de temps pour s'imposer.
Prenons l'exemple des 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
|}
Les CPU superscalaires dupliquent l'unité entière en 4 à 6 exemplaires. En général, les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Par exemple, un CPU à 6 voies pourra émettre/exécuter 6 opérations entières simultanément, grâce à 6 ALU entières. Mais il s'agit là d'un maximum et certaines peuvent être remplacées par une opération flottante ou mémoire, qui utilisent l'unité mémoire et/ou la FPU.
Un exemple assez parlant est les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon. C'était une microarchitecture à triple émission, avec trois ALU entières et trois unités mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements.
Il s'agit là d'une tendance, cependant. Les CPU superscalaires tendent à mettre le paquet niveau opérations arithmétiques, mais pas forcément au point de remplir la totalité de la largeur du processeur. La règle est que pour N voies, le processeur dispose d'au moins N/2 ALU entières. Attention, cela ne signifie pas que le processeur émet systématiquement N/2 opération entières. Il peut par exemple émettre une combinaison d'opérations flottantes et mémoire, avec des branchements. Et N/2 est une borne basse, la plupart des CPU x86 ont environ tournent plutôt autour des 2/3, voire des 3/4.
En général, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. La conséquence est que les unités mémoire sont dupliquées, en seconde place dans l'ordre d'importance. Les processeurs x86 ont commencé à dupliquer leurs unités mémoire dès qu'ils ont atteint la quadruple émission, pour les CPU grand public. La microarchitecture Core d'Intel utilisait la quadruple émission, avec deux unités mémoire et trois ALU entières. Les microarchitectures d'AMD K5 et K6 utilisaient aussi la quadruple émission et avaient deux ALUs pour deux unités mémoire.
Plus récemment, la microarchitecture ''Golden Cove'' dispose de cinq unités de calcul trois unités mémoire (deux spécialisées dans les écritures, trois dans les lectures). La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire.Il y a donc soit égalité, soit avantage pour les ALU entières.
Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair.
==L'évolution historique de la superscalarité==
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. La plupart des CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 sont rapidement montés à de la quadruple émission, avec quelques exceptions. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 ||
|-
! 1995
| 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000
|-
! 1996
| class="f_bleu | 620 || || || || class="f_bleu | 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
===Les processeurs historiques à double émission===
Pour commencer, étudions le cas des premiers CPU superscalaires à double émission. Ils sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Notamment, la duplication des autres unités ne sert pour ainsi dire à rien, ce qui limite les possibilités, mais rendra les explications plus simples.
Il pouvaient se classer en trois types principaux, avec quelques processeurs qui ne rentraient pas dans ces cases. Le premier type utilise un ''partitionnement complet'' entre ALU, FPU et unité mémoire. Le second utilisait la double émission entière-flottante. Le troisième utilisait la double émission entière. Voici le tableau chronologique disant quel CPU utilisait quelle technique.
* Le partitionnement INT-FLOAT-MEM est en jaune.
* La double émission entière-flottante est en vert.
* La double émission entière est en rouge.
* Les CPU barrés utilisent la quadruple émission et seront vu dans la section suivante.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| class="f_vert | RS/6000 || || || class="f_rouge | i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| class="f_vert | RSC || class="f_vert | PA 7100 || SuperSPARC || || class="f_jaune | 21064 ||
|-
! 1993
| class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || ||
|-
! 1994
| <s>POWER 2</s> || PA 7200 || || || <s>21164</s> ||
|}
Les processeurs laissés de côté dans le tableau précédent ne rentrent pas dans les catégories précédentes.
Le PA 7200 est un PA-7100 auquel on aurait rajouté une ALU entière. Le processeur exécutait les multiplications entières dans la FPU, comme son prédécesseur. Les combinaisons autorisées étaient donc INT + INT, INT + MEM, INT + FLOAT/MUL, MEM + FLOAT/MUL.
Le SuperSPARC était un CPU triple émission, avec deux ALU entières, une FPU et une unité mémoire. Il avait de nombreuses contraintes d’appariement. Par exemple, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Une bonne partie de ces contraintes était lié au banc de registre, qui n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions. Il faut noter que la seconde ALU a été abandonnée lors du passage au CPU HyperSPARC, qui est de plus repassé en double émission, ce qui est une évolution à rebours de l'évolution des autres CPUs.
===Les processeurs historiques à quadruple émission===
C'est la même année , en 1994, que la quadruple émission est arrivée et que les CPU ont commencé à avoir de plus en plus d'unités de calcul. Même les designs restés en double émission avaient ajouté des unités de calcul. Il y avait alors trois stratégies pour la quadruple émission.
La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Les FPUs étaient soit dupliquées, soit scindées en deux en séparant l'additionneur du multiplieur flottant. Le POWER 2 dupliquait ses FPUs à la perfection, alors que l'21264 partitionnait la FPU. Les accès mémoire étaient traités comme des opérations entières, et le processeur pouvait : soit en faire un seul par cycle, soit en émettre deux, sous conditions (le cache devait être double port pour lire/écrire deux données à la fois, comme on le verra dans la suite du chapitre).
Les processeurs MIPS R8000 et R10000 allaient un peu plus loin, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités.
La seconde implémentation dupliquait l'ALU entière et combinait le tout avec un partitionnement INT-FLOAT-MEM. De quoi exécuter en même temps deux opérations entières, une opération flottante, et un accès mémoire. 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).
Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, avec au minimum deux ALU entières. Pour comprendre ce que je veux dire, prenons l'exemple de l'UltraSPARC. Il avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplier flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes. Le processeur pouvait émettre 4 instructions, tant qu'elles atterrissaient dans des unités différentes.
La même méthode peut être utilisée pour la double émission. Le Motorola 88110, sorti en 1991, était un CPU à double émission avec pas moins de 10 unités : deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise.
La morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes. Le tout est résumé dans le tableau ci-dessous, avec :
* en vert les CPU utilisant la première stratégie ;
* en rouge les CPU utilisant la seconde stratégie ;
* en jaune les CPU utilisant la troisième stratégie.
{|class="wikitable"
|+ Processeurs quadruple émission historiques
|-
! Année !! Processeur !! Unités
|-
! 1991
| class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques
|-
|-
! rowspan="2" | 1994
| class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'')
|-
| class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM
|-
! rowspan="3" | 1995
| class="f_jaune | PA 8000 || class="f_jaune | 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD
|-
| class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques
|-
| MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM
|-
! rowspan="2" | 1996
| class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM
|-
| class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL)
|}
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les CPU superscalaire à exécution dans le désordre==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Les unités d'exécution dans le désordre===
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports de ''dispatch''. Mais avec des stations de réservation, il faut le double des ports de ''schedule''. La raison est que les registres sont lus après émission avec une fenê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 de ''schedule'' que de ''dispatch''.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de 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===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Avec plusieurs dizaines d'unités de calcul différentes, ajouter autant 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.
La première solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Le processeur détecte quand il n'y a pas assez de ports pour servir toutes les micro-opérations. Quand ça arrive, l'unité d'émission émet assez de micro-opérations pour exploiter les ports disponibles, mais met en attente les micro-opérations en trop, le temps que les ports se libèrent. Pour cela, le banc de registre est géré par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Une autre solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une dernière solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
0s2dn7akapv9rg4gcucl490d12r7gxq
773183
773182
2026-09-25T20:27:09Z
Mewtow
31375
/* L'évolution historique de la superscalarité */
773183
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===L'impact des contraintes d’appariement sur la performance===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour la suite du chapitre===
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==Le ''front-end'' des processeurs superscalaires==
Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple.
Intuitivement, charger, décoder et 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 certaines processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. 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
|}
Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs.
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===L'émission parallèle des instructions===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elle sont propagée au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage).
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
==Les processeurs implémentés avec plusieurs pipelines==
Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Vous vous dites que l'implémentation d'un processeur superscalaire n'est pas la même suivant que le CPU utilise l'exécution dans le désordre, ou non. Mais dans les faits, la distinction est plus subtile que ca. Pour l'expliquer, je vais utiliser une classification originale, qui sépare les processeurs superscalaires en trois sous-types. La distinction se fait sur la manière dont la superscalarité est implémentée en matériel.La classification que j'utilise distingue :
* Les processeurs avec plusieurs pipelines, sans exécution dans le désordre.
* Les processeurs superscalaires avec un pipeline dynamique.
* Les processeurs avec des fenêtres d'instruction décentralisées.
La distinction n'est pas qu'une question d'implémentation matérielle. Elle impacte aussi le ratio entre instructions chargées/décodées et instructions émises. Vous vous dites que les deux sont égales, mais ce n'est le cas que sur les processeurs avec N pipelines ! Sur les autres processeurs, il est possible de décoder 4 instructions, mais d'en émettre maximum 6 par cycle ! Et sur les CPU avec des fenêtres d'instruction décentralisées, il y a aussi une différence entre le nombre d'instructions qui sont ''dispatch'' et ''schedule''.
La catégorie la plus simple est celle des '''processeurs avec plusieurs pipelines'''. Ce sont des processeurs superscalaires sans exécution dans le désordre. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction.
Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, d'une FPU, éventuellement d'un second banc de registres, guère plus. Il permet d'exécuter une seconde opération de calcul, pas plus, ce qui est à l'origine de contraintes d'appariemment assez fortes. De plus, il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section.
===La double émission entière-flottante===
L'exemple le plus simple utilise un pipeline pour les opérations flottantes, séparé du pipeline originel. Pour bien expliquer les choses, prenons le processeur illustré ci-dessous. Un processeur a deux ''back-end'' séparés, un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par l'ALU entière, passent éventuellement dans l'unité mémoire, pour ensuite arriver à l'étape de ''writeback''. La FPU et le banc de registre flottant ont eux aussi leur propre ''back-end'' séparé.
[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]]
Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaqaue pipeline, à chaque cycle.
L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine.
Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants.
===La double émission entière===
Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait.
Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
==Les processeurs superscalaires dynamiques==
Les processeurs précédents, avec 2 pipelines, avaient une unité d'émission assez simple. Elle avait deux ports de décodage, et deux ports d'émission. Le nombre de ports de décodage et d'émission étaient identiques. Vous vous dites que c'est naturel, et intuitif, que c'est normal pour un processeur superscalaire. Mais en réalité, la majorité des processeurs superscalaires a plus de ports d'émission que de décodage. Expliquer pourquoi va cependant nous demander de parler de comment la superscalarité exploite les unités de calcul.
Posons-nous cette question : de combien d'unités un processeur superscalaire a besoin ? Les unités en question ne sont nécessairement des unité de calcul, l'unité mémoire et l'unité de branchement comptent aussi. La réponse dépend du nombre de voies. Si le ''front-end'' charge et décode un paquet de N instructions, exécuter ces N instructions simultanément demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section.
La réponse dépend aussi des contraintes d’appariement. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission parfaite devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement, ce qui ferait passer de 6 unités à 12. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, le cout en circuit encore plus prohibitif !
En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée, alors que la double émission parait plus adaptée.
Et c'est là que la différence entre ports d'émission et de décodage devient intéressante. L'idée est d'avoir une unité d'émission avec deux ports de décodage, et 4 port d'émission (un par unité). Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux micro-opérations tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu.
Les tout premiers CPU superscalaires étaient nombreux à utiliser cette technique. Les exemples classiques sont l'HyperSPARC, de l'alpha 21064 et du PowerPC 603. C'étaient des CPU double émission, avec une ALU entière, une unité mémoire et une FPU. Nous parlerons de '''partitionnement INT-FLOAT-MEM'''. Les contraintes d’appariement variaient grandement suivant le processeur.
Le PowerPC 603 implémentait une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur.
Avant de poursuivre, précisons que la FPU regroupe un additionneur flottant et un multiplieur flottant. Et ce fait peut être exploité pour approfondir le partitionnement. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions).
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple, facile à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. La raison est que le partitionnement permet d'émettre des paires, triplets, quadruplets de micro-opérations qui sont rares en pratique.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''.
Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, 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. Et enfin, il faut rajouter un port d'émission pour pouvoir émettre une micro-opération entière en plus.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle.
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
Reste à connecter les unités de calcul à l'unité d'émission (indirectement, car il y a des étages de pipeline entre les deux). Une implémentation basique utilisee un port d'émission par unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La présence d'un circuit multiplieur et d'un ''barrel shifter'' pose des problèmes pour le partitionnement. Il est possible d'avoir un port d'émission dédié au multiplieur, et un autre au ''barrel shifter'', qui sont séparés de ceux pour l'ALU entière. Il est alors possible d'émettre une opération entière, une multiplication et un décalage, simultanément. Nous parlerons de '''partitionnement entier''' pour désigner cette possibilité.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
Le partitionnement entier est cependant très rare, car le gain en performance serait trop faible au regard des couts. Quelques processeurs historiques l'utilisaient, mais ce n'est plus le cas de nos jours. La raison est que les multiplications et les décalages sont assez rares dans le code, ce qui fait que le port d'émission associés sont sous-utilisés. Et un port d'émission a un cout en circuit assez important. 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.
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul. L'exemple classique est justement lié aux circuits multiplieur et ''barrel shifters''. L'idée est que le multiplieur et le ''barrel shifter'' sont mis sur le même port d'émission qu'une ALU entière.
Prenons par exemple le processeur à double émission entière illustré ci-contre. Un port d'émission est relié à une ALU entière seule, et l'autre port d'émission alimente la seconde ALU entière et le multiplieur. La conséquence est que le processeur peut émettre deux opérations entières, dont une multiplication. Si on avait utilisé un port d'émission dédié au multiplieur, il aurait été possible d'émettre deux opérations entières et une multiplication. On perd donc une voie, mais on gagne en flexibilité et on économise des circuits.
[[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]]
En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais le partage des ports d'émission peut appliquer à n'importe quelle unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. 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.
La solution est de partager des ports d'émission, pour passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple relie une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires mélangent les trois techniques===
Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autre unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents.
* Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU.
* La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants.
* La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables.
* Le '''partage des ports d'émission''' réduit le cout en circuit du CPU, pour des pertes de performances mineures si bien fait.
De nos jours, le partitionnement est utilisé en complément de la duplication des ALU entières, de la duplication des autres unités, et du partage des ports d'émission. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. Le partage des ports d'émission a lui aussi mis un peu de temps pour s'imposer.
Prenons l'exemple des 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
|}
Les CPU superscalaires dupliquent l'unité entière en 4 à 6 exemplaires. En général, les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Par exemple, un CPU à 6 voies pourra émettre/exécuter 6 opérations entières simultanément, grâce à 6 ALU entières. Mais il s'agit là d'un maximum et certaines peuvent être remplacées par une opération flottante ou mémoire, qui utilisent l'unité mémoire et/ou la FPU.
Un exemple assez parlant est les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon. C'était une microarchitecture à triple émission, avec trois ALU entières et trois unités mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements.
Il s'agit là d'une tendance, cependant. Les CPU superscalaires tendent à mettre le paquet niveau opérations arithmétiques, mais pas forcément au point de remplir la totalité de la largeur du processeur. La règle est que pour N voies, le processeur dispose d'au moins N/2 ALU entières. Attention, cela ne signifie pas que le processeur émet systématiquement N/2 opération entières. Il peut par exemple émettre une combinaison d'opérations flottantes et mémoire, avec des branchements. Et N/2 est une borne basse, la plupart des CPU x86 ont environ tournent plutôt autour des 2/3, voire des 3/4.
En général, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. La conséquence est que les unités mémoire sont dupliquées, en seconde place dans l'ordre d'importance. Les processeurs x86 ont commencé à dupliquer leurs unités mémoire dès qu'ils ont atteint la quadruple émission, pour les CPU grand public. La microarchitecture Core d'Intel utilisait la quadruple émission, avec deux unités mémoire et trois ALU entières. Les microarchitectures d'AMD K5 et K6 utilisaient aussi la quadruple émission et avaient deux ALUs pour deux unités mémoire.
Plus récemment, la microarchitecture ''Golden Cove'' dispose de cinq unités de calcul trois unités mémoire (deux spécialisées dans les écritures, trois dans les lectures). La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire.Il y a donc soit égalité, soit avantage pour les ALU entières.
Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les CPU superscalaire à exécution dans le désordre==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Les unités d'exécution dans le désordre===
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports de ''dispatch''. Mais avec des stations de réservation, il faut le double des ports de ''schedule''. La raison est que les registres sont lus après émission avec une fenê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 de ''schedule'' que de ''dispatch''.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de 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===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Avec plusieurs dizaines d'unités de calcul différentes, ajouter autant 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.
La première solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Le processeur détecte quand il n'y a pas assez de ports pour servir toutes les micro-opérations. Quand ça arrive, l'unité d'émission émet assez de micro-opérations pour exploiter les ports disponibles, mais met en attente les micro-opérations en trop, le temps que les ports se libèrent. Pour cela, le banc de registre est géré par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Une autre solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une dernière solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
7dgw133kfbywfr4wwrm1kqoprcvahqi
773184
773183
2026-09-25T20:27:21Z
Mewtow
31375
/* La micro-fusion des processeurs Intel et AMD */
773184
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===L'impact des contraintes d’appariement sur la performance===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour la suite du chapitre===
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==Le ''front-end'' des processeurs superscalaires==
Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple.
Intuitivement, charger, décoder et 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 certaines processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. 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
|}
Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs.
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===L'émission parallèle des instructions===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elle sont propagée au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage).
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
==Les processeurs implémentés avec plusieurs pipelines==
Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Vous vous dites que l'implémentation d'un processeur superscalaire n'est pas la même suivant que le CPU utilise l'exécution dans le désordre, ou non. Mais dans les faits, la distinction est plus subtile que ca. Pour l'expliquer, je vais utiliser une classification originale, qui sépare les processeurs superscalaires en trois sous-types. La distinction se fait sur la manière dont la superscalarité est implémentée en matériel.La classification que j'utilise distingue :
* Les processeurs avec plusieurs pipelines, sans exécution dans le désordre.
* Les processeurs superscalaires avec un pipeline dynamique.
* Les processeurs avec des fenêtres d'instruction décentralisées.
La distinction n'est pas qu'une question d'implémentation matérielle. Elle impacte aussi le ratio entre instructions chargées/décodées et instructions émises. Vous vous dites que les deux sont égales, mais ce n'est le cas que sur les processeurs avec N pipelines ! Sur les autres processeurs, il est possible de décoder 4 instructions, mais d'en émettre maximum 6 par cycle ! Et sur les CPU avec des fenêtres d'instruction décentralisées, il y a aussi une différence entre le nombre d'instructions qui sont ''dispatch'' et ''schedule''.
La catégorie la plus simple est celle des '''processeurs avec plusieurs pipelines'''. Ce sont des processeurs superscalaires sans exécution dans le désordre. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction.
Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, d'une FPU, éventuellement d'un second banc de registres, guère plus. Il permet d'exécuter une seconde opération de calcul, pas plus, ce qui est à l'origine de contraintes d'appariemment assez fortes. De plus, il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section.
===La double émission entière-flottante===
L'exemple le plus simple utilise un pipeline pour les opérations flottantes, séparé du pipeline originel. Pour bien expliquer les choses, prenons le processeur illustré ci-dessous. Un processeur a deux ''back-end'' séparés, un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par l'ALU entière, passent éventuellement dans l'unité mémoire, pour ensuite arriver à l'étape de ''writeback''. La FPU et le banc de registre flottant ont eux aussi leur propre ''back-end'' séparé.
[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]]
Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaqaue pipeline, à chaque cycle.
L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine.
Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants.
===La double émission entière===
Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait.
Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
==Les processeurs superscalaires dynamiques==
Les processeurs précédents, avec 2 pipelines, avaient une unité d'émission assez simple. Elle avait deux ports de décodage, et deux ports d'émission. Le nombre de ports de décodage et d'émission étaient identiques. Vous vous dites que c'est naturel, et intuitif, que c'est normal pour un processeur superscalaire. Mais en réalité, la majorité des processeurs superscalaires a plus de ports d'émission que de décodage. Expliquer pourquoi va cependant nous demander de parler de comment la superscalarité exploite les unités de calcul.
Posons-nous cette question : de combien d'unités un processeur superscalaire a besoin ? Les unités en question ne sont nécessairement des unité de calcul, l'unité mémoire et l'unité de branchement comptent aussi. La réponse dépend du nombre de voies. Si le ''front-end'' charge et décode un paquet de N instructions, exécuter ces N instructions simultanément demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section.
La réponse dépend aussi des contraintes d’appariement. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission parfaite devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement, ce qui ferait passer de 6 unités à 12. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, le cout en circuit encore plus prohibitif !
En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée, alors que la double émission parait plus adaptée.
Et c'est là que la différence entre ports d'émission et de décodage devient intéressante. L'idée est d'avoir une unité d'émission avec deux ports de décodage, et 4 port d'émission (un par unité). Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux micro-opérations tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu.
Les tout premiers CPU superscalaires étaient nombreux à utiliser cette technique. Les exemples classiques sont l'HyperSPARC, de l'alpha 21064 et du PowerPC 603. C'étaient des CPU double émission, avec une ALU entière, une unité mémoire et une FPU. Nous parlerons de '''partitionnement INT-FLOAT-MEM'''. Les contraintes d’appariement variaient grandement suivant le processeur.
Le PowerPC 603 implémentait une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur.
Avant de poursuivre, précisons que la FPU regroupe un additionneur flottant et un multiplieur flottant. Et ce fait peut être exploité pour approfondir le partitionnement. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions).
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple, facile à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. La raison est que le partitionnement permet d'émettre des paires, triplets, quadruplets de micro-opérations qui sont rares en pratique.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''.
Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, 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. Et enfin, il faut rajouter un port d'émission pour pouvoir émettre une micro-opération entière en plus.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle.
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
Reste à connecter les unités de calcul à l'unité d'émission (indirectement, car il y a des étages de pipeline entre les deux). Une implémentation basique utilisee un port d'émission par unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La présence d'un circuit multiplieur et d'un ''barrel shifter'' pose des problèmes pour le partitionnement. Il est possible d'avoir un port d'émission dédié au multiplieur, et un autre au ''barrel shifter'', qui sont séparés de ceux pour l'ALU entière. Il est alors possible d'émettre une opération entière, une multiplication et un décalage, simultanément. Nous parlerons de '''partitionnement entier''' pour désigner cette possibilité.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
Le partitionnement entier est cependant très rare, car le gain en performance serait trop faible au regard des couts. Quelques processeurs historiques l'utilisaient, mais ce n'est plus le cas de nos jours. La raison est que les multiplications et les décalages sont assez rares dans le code, ce qui fait que le port d'émission associés sont sous-utilisés. Et un port d'émission a un cout en circuit assez important. 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.
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul. L'exemple classique est justement lié aux circuits multiplieur et ''barrel shifters''. L'idée est que le multiplieur et le ''barrel shifter'' sont mis sur le même port d'émission qu'une ALU entière.
Prenons par exemple le processeur à double émission entière illustré ci-contre. Un port d'émission est relié à une ALU entière seule, et l'autre port d'émission alimente la seconde ALU entière et le multiplieur. La conséquence est que le processeur peut émettre deux opérations entières, dont une multiplication. Si on avait utilisé un port d'émission dédié au multiplieur, il aurait été possible d'émettre deux opérations entières et une multiplication. On perd donc une voie, mais on gagne en flexibilité et on économise des circuits.
[[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]]
En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais le partage des ports d'émission peut appliquer à n'importe quelle unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. 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.
La solution est de partager des ports d'émission, pour passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple relie une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires mélangent les trois techniques===
Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autre unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents.
* Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU.
* La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants.
* La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables.
* Le '''partage des ports d'émission''' réduit le cout en circuit du CPU, pour des pertes de performances mineures si bien fait.
De nos jours, le partitionnement est utilisé en complément de la duplication des ALU entières, de la duplication des autres unités, et du partage des ports d'émission. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. Le partage des ports d'émission a lui aussi mis un peu de temps pour s'imposer.
Prenons l'exemple des 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
|}
Les CPU superscalaires dupliquent l'unité entière en 4 à 6 exemplaires. En général, les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Par exemple, un CPU à 6 voies pourra émettre/exécuter 6 opérations entières simultanément, grâce à 6 ALU entières. Mais il s'agit là d'un maximum et certaines peuvent être remplacées par une opération flottante ou mémoire, qui utilisent l'unité mémoire et/ou la FPU.
Un exemple assez parlant est les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon. C'était une microarchitecture à triple émission, avec trois ALU entières et trois unités mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements.
Il s'agit là d'une tendance, cependant. Les CPU superscalaires tendent à mettre le paquet niveau opérations arithmétiques, mais pas forcément au point de remplir la totalité de la largeur du processeur. La règle est que pour N voies, le processeur dispose d'au moins N/2 ALU entières. Attention, cela ne signifie pas que le processeur émet systématiquement N/2 opération entières. Il peut par exemple émettre une combinaison d'opérations flottantes et mémoire, avec des branchements. Et N/2 est une borne basse, la plupart des CPU x86 ont environ tournent plutôt autour des 2/3, voire des 3/4.
En général, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. La conséquence est que les unités mémoire sont dupliquées, en seconde place dans l'ordre d'importance. Les processeurs x86 ont commencé à dupliquer leurs unités mémoire dès qu'ils ont atteint la quadruple émission, pour les CPU grand public. La microarchitecture Core d'Intel utilisait la quadruple émission, avec deux unités mémoire et trois ALU entières. Les microarchitectures d'AMD K5 et K6 utilisaient aussi la quadruple émission et avaient deux ALUs pour deux unités mémoire.
Plus récemment, la microarchitecture ''Golden Cove'' dispose de cinq unités de calcul trois unités mémoire (deux spécialisées dans les écritures, trois dans les lectures). La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire.Il y a donc soit égalité, soit avantage pour les ALU entières.
Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les CPU superscalaire à exécution dans le désordre==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Les unités d'exécution dans le désordre===
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports de ''dispatch''. Mais avec des stations de réservation, il faut le double des ports de ''schedule''. La raison est que les registres sont lus après émission avec une fenê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 de ''schedule'' que de ''dispatch''.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de 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===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Avec plusieurs dizaines d'unités de calcul différentes, ajouter autant 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.
La première solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Le processeur détecte quand il n'y a pas assez de ports pour servir toutes les micro-opérations. Quand ça arrive, l'unité d'émission émet assez de micro-opérations pour exploiter les ports disponibles, mais met en attente les micro-opérations en trop, le temps que les ports se libèrent. Pour cela, le banc de registre est géré par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Une autre solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une dernière solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
==L'évolution historique de la superscalarité==
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. La plupart des CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 sont rapidement montés à de la quadruple émission, avec quelques exceptions. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 ||
|-
! 1995
| 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000
|-
! 1996
| class="f_bleu | 620 || || || || class="f_bleu | 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
===Les processeurs historiques à double émission===
Pour commencer, étudions le cas des premiers CPU superscalaires à double émission. Ils sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Notamment, la duplication des autres unités ne sert pour ainsi dire à rien, ce qui limite les possibilités, mais rendra les explications plus simples.
Il pouvaient se classer en trois types principaux, avec quelques processeurs qui ne rentraient pas dans ces cases. Le premier type utilise un ''partitionnement complet'' entre ALU, FPU et unité mémoire. Le second utilisait la double émission entière-flottante. Le troisième utilisait la double émission entière. Voici le tableau chronologique disant quel CPU utilisait quelle technique.
* Le partitionnement INT-FLOAT-MEM est en jaune.
* La double émission entière-flottante est en vert.
* La double émission entière est en rouge.
* Les CPU barrés utilisent la quadruple émission et seront vu dans la section suivante.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| class="f_vert | RS/6000 || || || class="f_rouge | i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| class="f_vert | RSC || class="f_vert | PA 7100 || SuperSPARC || || class="f_jaune | 21064 ||
|-
! 1993
| class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || ||
|-
! 1994
| <s>POWER 2</s> || PA 7200 || || || <s>21164</s> ||
|}
Les processeurs laissés de côté dans le tableau précédent ne rentrent pas dans les catégories précédentes.
Le PA 7200 est un PA-7100 auquel on aurait rajouté une ALU entière. Le processeur exécutait les multiplications entières dans la FPU, comme son prédécesseur. Les combinaisons autorisées étaient donc INT + INT, INT + MEM, INT + FLOAT/MUL, MEM + FLOAT/MUL.
Le SuperSPARC était un CPU triple émission, avec deux ALU entières, une FPU et une unité mémoire. Il avait de nombreuses contraintes d’appariement. Par exemple, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Une bonne partie de ces contraintes était lié au banc de registre, qui n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions. Il faut noter que la seconde ALU a été abandonnée lors du passage au CPU HyperSPARC, qui est de plus repassé en double émission, ce qui est une évolution à rebours de l'évolution des autres CPUs.
===Les processeurs historiques à quadruple émission===
C'est la même année , en 1994, que la quadruple émission est arrivée et que les CPU ont commencé à avoir de plus en plus d'unités de calcul. Même les designs restés en double émission avaient ajouté des unités de calcul. Il y avait alors trois stratégies pour la quadruple émission.
La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Les FPUs étaient soit dupliquées, soit scindées en deux en séparant l'additionneur du multiplieur flottant. Le POWER 2 dupliquait ses FPUs à la perfection, alors que l'21264 partitionnait la FPU. Les accès mémoire étaient traités comme des opérations entières, et le processeur pouvait : soit en faire un seul par cycle, soit en émettre deux, sous conditions (le cache devait être double port pour lire/écrire deux données à la fois, comme on le verra dans la suite du chapitre).
Les processeurs MIPS R8000 et R10000 allaient un peu plus loin, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités.
La seconde implémentation dupliquait l'ALU entière et combinait le tout avec un partitionnement INT-FLOAT-MEM. De quoi exécuter en même temps deux opérations entières, une opération flottante, et un accès mémoire. 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).
Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, avec au minimum deux ALU entières. Pour comprendre ce que je veux dire, prenons l'exemple de l'UltraSPARC. Il avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplier flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes. Le processeur pouvait émettre 4 instructions, tant qu'elles atterrissaient dans des unités différentes.
La même méthode peut être utilisée pour la double émission. Le Motorola 88110, sorti en 1991, était un CPU à double émission avec pas moins de 10 unités : deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise.
La morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes. Le tout est résumé dans le tableau ci-dessous, avec :
* en vert les CPU utilisant la première stratégie ;
* en rouge les CPU utilisant la seconde stratégie ;
* en jaune les CPU utilisant la troisième stratégie.
{|class="wikitable"
|+ Processeurs quadruple émission historiques
|-
! Année !! Processeur !! Unités
|-
! 1991
| class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques
|-
|-
! rowspan="2" | 1994
| class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'')
|-
| class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM
|-
! rowspan="3" | 1995
| class="f_jaune | PA 8000 || class="f_jaune | 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD
|-
| class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques
|-
| MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM
|-
! rowspan="2" | 1996
| class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM
|-
| class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL)
|}
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
rk3g7mqeigqhchakubywi6vh1f9m2z1
773185
773184
2026-09-25T20:30:42Z
Mewtow
31375
/* Les CPU superscalaires mélangent les trois techniques */
773185
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===L'impact des contraintes d’appariement sur la performance===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour la suite du chapitre===
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==Le ''front-end'' des processeurs superscalaires==
Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple.
Intuitivement, charger, décoder et 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 certaines processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. 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
|}
Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs.
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===L'émission parallèle des instructions===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elle sont propagée au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage).
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
==Les processeurs implémentés avec plusieurs pipelines==
Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Vous vous dites que l'implémentation d'un processeur superscalaire n'est pas la même suivant que le CPU utilise l'exécution dans le désordre, ou non. Mais dans les faits, la distinction est plus subtile que ca. Pour l'expliquer, je vais utiliser une classification originale, qui sépare les processeurs superscalaires en trois sous-types. La distinction se fait sur la manière dont la superscalarité est implémentée en matériel.La classification que j'utilise distingue :
* Les processeurs avec plusieurs pipelines, sans exécution dans le désordre.
* Les processeurs superscalaires avec un pipeline dynamique.
* Les processeurs avec des fenêtres d'instruction décentralisées.
La distinction n'est pas qu'une question d'implémentation matérielle. Elle impacte aussi le ratio entre instructions chargées/décodées et instructions émises. Vous vous dites que les deux sont égales, mais ce n'est le cas que sur les processeurs avec N pipelines ! Sur les autres processeurs, il est possible de décoder 4 instructions, mais d'en émettre maximum 6 par cycle ! Et sur les CPU avec des fenêtres d'instruction décentralisées, il y a aussi une différence entre le nombre d'instructions qui sont ''dispatch'' et ''schedule''.
La catégorie la plus simple est celle des '''processeurs avec plusieurs pipelines'''. Ce sont des processeurs superscalaires sans exécution dans le désordre. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction.
Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, d'une FPU, éventuellement d'un second banc de registres, guère plus. Il permet d'exécuter une seconde opération de calcul, pas plus, ce qui est à l'origine de contraintes d'appariemment assez fortes. De plus, il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section.
===La double émission entière-flottante===
L'exemple le plus simple utilise un pipeline pour les opérations flottantes, séparé du pipeline originel. Pour bien expliquer les choses, prenons le processeur illustré ci-dessous. Un processeur a deux ''back-end'' séparés, un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par l'ALU entière, passent éventuellement dans l'unité mémoire, pour ensuite arriver à l'étape de ''writeback''. La FPU et le banc de registre flottant ont eux aussi leur propre ''back-end'' séparé.
[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]]
Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaqaue pipeline, à chaque cycle.
L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine.
Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants.
===La double émission entière===
Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait.
Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
==Les processeurs superscalaires dynamiques==
Les processeurs précédents, avec 2 pipelines, avaient une unité d'émission assez simple. Elle avait deux ports de décodage, et deux ports d'émission. Le nombre de ports de décodage et d'émission étaient identiques. Vous vous dites que c'est naturel, et intuitif, que c'est normal pour un processeur superscalaire. Mais en réalité, la majorité des processeurs superscalaires a plus de ports d'émission que de décodage. Expliquer pourquoi va cependant nous demander de parler de comment la superscalarité exploite les unités de calcul.
Posons-nous cette question : de combien d'unités un processeur superscalaire a besoin ? Les unités en question ne sont nécessairement des unité de calcul, l'unité mémoire et l'unité de branchement comptent aussi. La réponse dépend du nombre de voies. Si le ''front-end'' charge et décode un paquet de N instructions, exécuter ces N instructions simultanément demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section.
La réponse dépend aussi des contraintes d’appariement. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission parfaite devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement, ce qui ferait passer de 6 unités à 12. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, le cout en circuit encore plus prohibitif !
En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée, alors que la double émission parait plus adaptée.
Et c'est là que la différence entre ports d'émission et de décodage devient intéressante. L'idée est d'avoir une unité d'émission avec deux ports de décodage, et 4 port d'émission (un par unité). Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux micro-opérations tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu.
Les tout premiers CPU superscalaires étaient nombreux à utiliser cette technique. Les exemples classiques sont l'HyperSPARC, de l'alpha 21064 et du PowerPC 603. C'étaient des CPU double émission, avec une ALU entière, une unité mémoire et une FPU. Nous parlerons de '''partitionnement INT-FLOAT-MEM'''. Les contraintes d’appariement variaient grandement suivant le processeur.
Le PowerPC 603 implémentait une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur.
Avant de poursuivre, précisons que la FPU regroupe un additionneur flottant et un multiplieur flottant. Et ce fait peut être exploité pour approfondir le partitionnement. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions).
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple, facile à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. La raison est que le partitionnement permet d'émettre des paires, triplets, quadruplets de micro-opérations qui sont rares en pratique.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''.
Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, 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. Et enfin, il faut rajouter un port d'émission pour pouvoir émettre une micro-opération entière en plus.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle.
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
Reste à connecter les unités de calcul à l'unité d'émission (indirectement, car il y a des étages de pipeline entre les deux). Une implémentation basique utilisee un port d'émission par unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La présence d'un circuit multiplieur et d'un ''barrel shifter'' pose des problèmes pour le partitionnement. Il est possible d'avoir un port d'émission dédié au multiplieur, et un autre au ''barrel shifter'', qui sont séparés de ceux pour l'ALU entière. Il est alors possible d'émettre une opération entière, une multiplication et un décalage, simultanément. Nous parlerons de '''partitionnement entier''' pour désigner cette possibilité.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
Le partitionnement entier est cependant très rare, car le gain en performance serait trop faible au regard des couts. Quelques processeurs historiques l'utilisaient, mais ce n'est plus le cas de nos jours. La raison est que les multiplications et les décalages sont assez rares dans le code, ce qui fait que le port d'émission associés sont sous-utilisés. Et un port d'émission a un cout en circuit assez important. 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.
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul. L'exemple classique est justement lié aux circuits multiplieur et ''barrel shifters''. L'idée est que le multiplieur et le ''barrel shifter'' sont mis sur le même port d'émission qu'une ALU entière.
Prenons par exemple le processeur à double émission entière illustré ci-contre. Un port d'émission est relié à une ALU entière seule, et l'autre port d'émission alimente la seconde ALU entière et le multiplieur. La conséquence est que le processeur peut émettre deux opérations entières, dont une multiplication. Si on avait utilisé un port d'émission dédié au multiplieur, il aurait été possible d'émettre deux opérations entières et une multiplication. On perd donc une voie, mais on gagne en flexibilité et on économise des circuits.
[[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]]
En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais le partage des ports d'émission peut appliquer à n'importe quelle unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. 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.
La solution est de partager des ports d'émission, pour passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple relie une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires mélangent les trois techniques===
Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autre unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents.
* Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU.
* La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants.
* La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables.
* Le '''partage des ports d'émission''' réduit le cout en circuit du CPU, pour des pertes de performances mineures si bien fait.
De nos jours, le partitionnement est utilisé en complément de la duplication des ALU entières, de la duplication des autres unités, et du partage des ports d'émission. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. Le partage des ports d'émission a lui aussi mis un peu de temps pour s'imposer.
Les processeurs superscalaires anciens dupliquaient l'ALU et utilisaient beaucoup de ports d'émission. La seule contrainte est que les multipliuers et les ''barrel shifter'' partageaient leur port d'émission avec une ALU. Prenons l'exemple des 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
|}
Un autre exemple est les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon. C'était une microarchitecture à triple émission, avec trois ALU entières et trois unités mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements.
Les CPU superscalaires tendent à mettre le paquet niveau opérations arithmétiques, mais pas forcément au point de remplir la totalité de la largeur du processeur. La règle est que pour N voies, le processeur dispose d'au moins N/2 ALU entières. Attention, cela ne signifie pas que le processeur émet systématiquement N/2 opération entières. Il peut par exemple émettre une combinaison d'opérations flottantes et mémoire, avec des branchements. Et N/2 est une borne basse, la plupart des CPU x86 ont environ tournent plutôt autour des 2/3, voire des 3/4.
En général, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. La conséquence est que les unités mémoire sont dupliquées, en seconde place dans l'ordre d'importance. Les processeurs x86 ont commencé à dupliquer leurs unités mémoire dès qu'ils ont atteint la quadruple émission, pour les CPU grand public. La microarchitecture Core d'Intel utilisait la quadruple émission, avec deux unités mémoire et trois ALU entières. Les microarchitectures d'AMD K5 et K6 utilisaient aussi la quadruple émission et avaient deux ALUs pour deux unités mémoire.
Plus récemment, la microarchitecture ''Golden Cove'' dispose de cinq unités de calcul trois unités mémoire (deux spécialisées dans les écritures, trois dans les lectures). La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire.Il y a donc soit égalité, soit avantage pour les ALU entières.
Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les CPU superscalaire à exécution dans le désordre==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Les unités d'exécution dans le désordre===
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports de ''dispatch''. Mais avec des stations de réservation, il faut le double des ports de ''schedule''. La raison est que les registres sont lus après émission avec une fenê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 de ''schedule'' que de ''dispatch''.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de 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===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Avec plusieurs dizaines d'unités de calcul différentes, ajouter autant 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.
La première solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Le processeur détecte quand il n'y a pas assez de ports pour servir toutes les micro-opérations. Quand ça arrive, l'unité d'émission émet assez de micro-opérations pour exploiter les ports disponibles, mais met en attente les micro-opérations en trop, le temps que les ports se libèrent. Pour cela, le banc de registre est géré par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Une autre solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une dernière solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
==L'évolution historique de la superscalarité==
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. La plupart des CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 sont rapidement montés à de la quadruple émission, avec quelques exceptions. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 ||
|-
! 1995
| 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000
|-
! 1996
| class="f_bleu | 620 || || || || class="f_bleu | 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
===Les processeurs historiques à double émission===
Pour commencer, étudions le cas des premiers CPU superscalaires à double émission. Ils sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Notamment, la duplication des autres unités ne sert pour ainsi dire à rien, ce qui limite les possibilités, mais rendra les explications plus simples.
Il pouvaient se classer en trois types principaux, avec quelques processeurs qui ne rentraient pas dans ces cases. Le premier type utilise un ''partitionnement complet'' entre ALU, FPU et unité mémoire. Le second utilisait la double émission entière-flottante. Le troisième utilisait la double émission entière. Voici le tableau chronologique disant quel CPU utilisait quelle technique.
* Le partitionnement INT-FLOAT-MEM est en jaune.
* La double émission entière-flottante est en vert.
* La double émission entière est en rouge.
* Les CPU barrés utilisent la quadruple émission et seront vu dans la section suivante.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| class="f_vert | RS/6000 || || || class="f_rouge | i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| class="f_vert | RSC || class="f_vert | PA 7100 || SuperSPARC || || class="f_jaune | 21064 ||
|-
! 1993
| class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || ||
|-
! 1994
| <s>POWER 2</s> || PA 7200 || || || <s>21164</s> ||
|}
Les processeurs laissés de côté dans le tableau précédent ne rentrent pas dans les catégories précédentes.
Le PA 7200 est un PA-7100 auquel on aurait rajouté une ALU entière. Le processeur exécutait les multiplications entières dans la FPU, comme son prédécesseur. Les combinaisons autorisées étaient donc INT + INT, INT + MEM, INT + FLOAT/MUL, MEM + FLOAT/MUL.
Le SuperSPARC était un CPU triple émission, avec deux ALU entières, une FPU et une unité mémoire. Il avait de nombreuses contraintes d’appariement. Par exemple, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Une bonne partie de ces contraintes était lié au banc de registre, qui n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions. Il faut noter que la seconde ALU a été abandonnée lors du passage au CPU HyperSPARC, qui est de plus repassé en double émission, ce qui est une évolution à rebours de l'évolution des autres CPUs.
===Les processeurs historiques à quadruple émission===
C'est la même année , en 1994, que la quadruple émission est arrivée et que les CPU ont commencé à avoir de plus en plus d'unités de calcul. Même les designs restés en double émission avaient ajouté des unités de calcul. Il y avait alors trois stratégies pour la quadruple émission.
La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Les FPUs étaient soit dupliquées, soit scindées en deux en séparant l'additionneur du multiplieur flottant. Le POWER 2 dupliquait ses FPUs à la perfection, alors que l'21264 partitionnait la FPU. Les accès mémoire étaient traités comme des opérations entières, et le processeur pouvait : soit en faire un seul par cycle, soit en émettre deux, sous conditions (le cache devait être double port pour lire/écrire deux données à la fois, comme on le verra dans la suite du chapitre).
Les processeurs MIPS R8000 et R10000 allaient un peu plus loin, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités.
La seconde implémentation dupliquait l'ALU entière et combinait le tout avec un partitionnement INT-FLOAT-MEM. De quoi exécuter en même temps deux opérations entières, une opération flottante, et un accès mémoire. 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).
Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, avec au minimum deux ALU entières. Pour comprendre ce que je veux dire, prenons l'exemple de l'UltraSPARC. Il avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplier flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes. Le processeur pouvait émettre 4 instructions, tant qu'elles atterrissaient dans des unités différentes.
La même méthode peut être utilisée pour la double émission. Le Motorola 88110, sorti en 1991, était un CPU à double émission avec pas moins de 10 unités : deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise.
La morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes. Le tout est résumé dans le tableau ci-dessous, avec :
* en vert les CPU utilisant la première stratégie ;
* en rouge les CPU utilisant la seconde stratégie ;
* en jaune les CPU utilisant la troisième stratégie.
{|class="wikitable"
|+ Processeurs quadruple émission historiques
|-
! Année !! Processeur !! Unités
|-
! 1991
| class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques
|-
|-
! rowspan="2" | 1994
| class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'')
|-
| class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM
|-
! rowspan="3" | 1995
| class="f_jaune | PA 8000 || class="f_jaune | 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD
|-
| class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques
|-
| MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM
|-
! rowspan="2" | 1996
| class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM
|-
| class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL)
|}
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
av6eghijchvd6jrc6mxkuijhl1chimh
773186
773185
2026-09-25T20:32:19Z
Mewtow
31375
/* Le partage des ports d'émission */
773186
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===L'impact des contraintes d’appariement sur la performance===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour la suite du chapitre===
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==Le ''front-end'' des processeurs superscalaires==
Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple.
Intuitivement, charger, décoder et 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 certaines processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. 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
|}
Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs.
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===L'émission parallèle des instructions===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elle sont propagée au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage).
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
==Les processeurs implémentés avec plusieurs pipelines==
Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Vous vous dites que l'implémentation d'un processeur superscalaire n'est pas la même suivant que le CPU utilise l'exécution dans le désordre, ou non. Mais dans les faits, la distinction est plus subtile que ca. Pour l'expliquer, je vais utiliser une classification originale, qui sépare les processeurs superscalaires en trois sous-types. La distinction se fait sur la manière dont la superscalarité est implémentée en matériel.La classification que j'utilise distingue :
* Les processeurs avec plusieurs pipelines, sans exécution dans le désordre.
* Les processeurs superscalaires avec un pipeline dynamique.
* Les processeurs avec des fenêtres d'instruction décentralisées.
La distinction n'est pas qu'une question d'implémentation matérielle. Elle impacte aussi le ratio entre instructions chargées/décodées et instructions émises. Vous vous dites que les deux sont égales, mais ce n'est le cas que sur les processeurs avec N pipelines ! Sur les autres processeurs, il est possible de décoder 4 instructions, mais d'en émettre maximum 6 par cycle ! Et sur les CPU avec des fenêtres d'instruction décentralisées, il y a aussi une différence entre le nombre d'instructions qui sont ''dispatch'' et ''schedule''.
La catégorie la plus simple est celle des '''processeurs avec plusieurs pipelines'''. Ce sont des processeurs superscalaires sans exécution dans le désordre. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction.
Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, d'une FPU, éventuellement d'un second banc de registres, guère plus. Il permet d'exécuter une seconde opération de calcul, pas plus, ce qui est à l'origine de contraintes d'appariemment assez fortes. De plus, il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section.
===La double émission entière-flottante===
L'exemple le plus simple utilise un pipeline pour les opérations flottantes, séparé du pipeline originel. Pour bien expliquer les choses, prenons le processeur illustré ci-dessous. Un processeur a deux ''back-end'' séparés, un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par l'ALU entière, passent éventuellement dans l'unité mémoire, pour ensuite arriver à l'étape de ''writeback''. La FPU et le banc de registre flottant ont eux aussi leur propre ''back-end'' séparé.
[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]]
Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaqaue pipeline, à chaque cycle.
L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine.
Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants.
===La double émission entière===
Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait.
Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
==Les processeurs superscalaires dynamiques==
Les processeurs précédents, avec 2 pipelines, avaient une unité d'émission assez simple. Elle avait deux ports de décodage, et deux ports d'émission. Le nombre de ports de décodage et d'émission étaient identiques. Vous vous dites que c'est naturel, et intuitif, que c'est normal pour un processeur superscalaire. Mais en réalité, la majorité des processeurs superscalaires a plus de ports d'émission que de décodage. Expliquer pourquoi va cependant nous demander de parler de comment la superscalarité exploite les unités de calcul.
Posons-nous cette question : de combien d'unités un processeur superscalaire a besoin ? Les unités en question ne sont nécessairement des unité de calcul, l'unité mémoire et l'unité de branchement comptent aussi. La réponse dépend du nombre de voies. Si le ''front-end'' charge et décode un paquet de N instructions, exécuter ces N instructions simultanément demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section.
La réponse dépend aussi des contraintes d’appariement. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission parfaite devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement, ce qui ferait passer de 6 unités à 12. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, le cout en circuit encore plus prohibitif !
En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée, alors que la double émission parait plus adaptée.
Et c'est là que la différence entre ports d'émission et de décodage devient intéressante. L'idée est d'avoir une unité d'émission avec deux ports de décodage, et 4 port d'émission (un par unité). Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux micro-opérations tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu.
Les tout premiers CPU superscalaires étaient nombreux à utiliser cette technique. Les exemples classiques sont l'HyperSPARC, de l'alpha 21064 et du PowerPC 603. C'étaient des CPU double émission, avec une ALU entière, une unité mémoire et une FPU. Nous parlerons de '''partitionnement INT-FLOAT-MEM'''. Les contraintes d’appariement variaient grandement suivant le processeur.
Le PowerPC 603 implémentait une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur.
Avant de poursuivre, précisons que la FPU regroupe un additionneur flottant et un multiplieur flottant. Et ce fait peut être exploité pour approfondir le partitionnement. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions).
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple, facile à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. La raison est que le partitionnement permet d'émettre des paires, triplets, quadruplets de micro-opérations qui sont rares en pratique.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''.
Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, 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. Et enfin, il faut rajouter un port d'émission pour pouvoir émettre une micro-opération entière en plus.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle.
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
Reste à connecter les unités de calcul à l'unité d'émission (indirectement, car il y a des étages de pipeline entre les deux). Une implémentation basique utilisee un port d'émission par unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La présence d'un circuit multiplieur et d'un ''barrel shifter'' pose des problèmes pour le partitionnement. Il est possible d'avoir un port d'émission dédié au multiplieur, et un autre au ''barrel shifter'', qui sont séparés de ceux pour l'ALU entière. Il est alors possible d'émettre une opération entière, une multiplication et un décalage, simultanément. Nous parlerons de '''partitionnement entier''' pour désigner cette possibilité.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
Le partitionnement entier est cependant très rare, car le gain en performance serait trop faible au regard des couts. Quelques processeurs historiques l'utilisaient, mais ce n'est plus le cas de nos jours. La raison est que les multiplications et les décalages sont assez rares dans le code, ce qui fait que le port d'émission associés sont sous-utilisés. Et un port d'émission a un cout en circuit assez important. 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.
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul. L'exemple classique est justement lié aux circuits multiplieur et ''barrel shifters''. L'idée est que le multiplieur et le ''barrel shifter'' sont mis sur le même port d'émission qu'une ALU entière.
Prenons par exemple le processeur à double émission entière illustré ci-contre. Un port d'émission est relié à une ALU entière seule, et l'autre port d'émission alimente la seconde ALU entière et le multiplieur. La conséquence est que le processeur peut émettre deux opérations entières, dont une multiplication. Si on avait utilisé un port d'émission dédié au multiplieur, il aurait été possible d'émettre deux opérations entières et une multiplication. On perd donc une voie, mais on gagne en flexibilité et on économise des circuits.
[[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]]
En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais le partage des ports d'émission peut appliquer à n'importe quelle unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. 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.
La solution est de partager des ports d'émission, pour passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple relie une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites. Le CPU peut émettre 6 opérations entières simultanément, grâce à 6 ALU entières. Les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires mélangent les trois techniques===
Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autre unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents.
* Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU.
* La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants.
* La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables.
* Le '''partage des ports d'émission''' réduit le cout en circuit du CPU, pour des pertes de performances mineures si bien fait.
De nos jours, le partitionnement est utilisé en complément de la duplication des ALU entières, de la duplication des autres unités, et du partage des ports d'émission. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. Le partage des ports d'émission a lui aussi mis un peu de temps pour s'imposer.
Les processeurs superscalaires anciens dupliquaient l'ALU et utilisaient beaucoup de ports d'émission. La seule contrainte est que les multipliuers et les ''barrel shifter'' partageaient leur port d'émission avec une ALU. Prenons l'exemple des 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
|}
Un autre exemple est les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon. C'était une microarchitecture à triple émission, avec trois ALU entières et trois unités mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements.
Les CPU superscalaires tendent à mettre le paquet niveau opérations arithmétiques, mais pas forcément au point de remplir la totalité de la largeur du processeur. La règle est que pour N voies, le processeur dispose d'au moins N/2 ALU entières. Attention, cela ne signifie pas que le processeur émet systématiquement N/2 opération entières. Il peut par exemple émettre une combinaison d'opérations flottantes et mémoire, avec des branchements. Et N/2 est une borne basse, la plupart des CPU x86 ont environ tournent plutôt autour des 2/3, voire des 3/4.
En général, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. La conséquence est que les unités mémoire sont dupliquées, en seconde place dans l'ordre d'importance. Les processeurs x86 ont commencé à dupliquer leurs unités mémoire dès qu'ils ont atteint la quadruple émission, pour les CPU grand public. La microarchitecture Core d'Intel utilisait la quadruple émission, avec deux unités mémoire et trois ALU entières. Les microarchitectures d'AMD K5 et K6 utilisaient aussi la quadruple émission et avaient deux ALUs pour deux unités mémoire.
Plus récemment, la microarchitecture ''Golden Cove'' dispose de cinq unités de calcul trois unités mémoire (deux spécialisées dans les écritures, trois dans les lectures). La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire.Il y a donc soit égalité, soit avantage pour les ALU entières.
Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les CPU superscalaire à exécution dans le désordre==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Les unités d'exécution dans le désordre===
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports de ''dispatch''. Mais avec des stations de réservation, il faut le double des ports de ''schedule''. La raison est que les registres sont lus après émission avec une fenê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 de ''schedule'' que de ''dispatch''.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de 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===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Avec plusieurs dizaines d'unités de calcul différentes, ajouter autant 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.
La première solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Le processeur détecte quand il n'y a pas assez de ports pour servir toutes les micro-opérations. Quand ça arrive, l'unité d'émission émet assez de micro-opérations pour exploiter les ports disponibles, mais met en attente les micro-opérations en trop, le temps que les ports se libèrent. Pour cela, le banc de registre est géré par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Une autre solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une dernière solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
==L'évolution historique de la superscalarité==
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. La plupart des CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 sont rapidement montés à de la quadruple émission, avec quelques exceptions. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 ||
|-
! 1995
| 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000
|-
! 1996
| class="f_bleu | 620 || || || || class="f_bleu | 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
===Les processeurs historiques à double émission===
Pour commencer, étudions le cas des premiers CPU superscalaires à double émission. Ils sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Notamment, la duplication des autres unités ne sert pour ainsi dire à rien, ce qui limite les possibilités, mais rendra les explications plus simples.
Il pouvaient se classer en trois types principaux, avec quelques processeurs qui ne rentraient pas dans ces cases. Le premier type utilise un ''partitionnement complet'' entre ALU, FPU et unité mémoire. Le second utilisait la double émission entière-flottante. Le troisième utilisait la double émission entière. Voici le tableau chronologique disant quel CPU utilisait quelle technique.
* Le partitionnement INT-FLOAT-MEM est en jaune.
* La double émission entière-flottante est en vert.
* La double émission entière est en rouge.
* Les CPU barrés utilisent la quadruple émission et seront vu dans la section suivante.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| class="f_vert | RS/6000 || || || class="f_rouge | i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| class="f_vert | RSC || class="f_vert | PA 7100 || SuperSPARC || || class="f_jaune | 21064 ||
|-
! 1993
| class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || ||
|-
! 1994
| <s>POWER 2</s> || PA 7200 || || || <s>21164</s> ||
|}
Les processeurs laissés de côté dans le tableau précédent ne rentrent pas dans les catégories précédentes.
Le PA 7200 est un PA-7100 auquel on aurait rajouté une ALU entière. Le processeur exécutait les multiplications entières dans la FPU, comme son prédécesseur. Les combinaisons autorisées étaient donc INT + INT, INT + MEM, INT + FLOAT/MUL, MEM + FLOAT/MUL.
Le SuperSPARC était un CPU triple émission, avec deux ALU entières, une FPU et une unité mémoire. Il avait de nombreuses contraintes d’appariement. Par exemple, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Une bonne partie de ces contraintes était lié au banc de registre, qui n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions. Il faut noter que la seconde ALU a été abandonnée lors du passage au CPU HyperSPARC, qui est de plus repassé en double émission, ce qui est une évolution à rebours de l'évolution des autres CPUs.
===Les processeurs historiques à quadruple émission===
C'est la même année , en 1994, que la quadruple émission est arrivée et que les CPU ont commencé à avoir de plus en plus d'unités de calcul. Même les designs restés en double émission avaient ajouté des unités de calcul. Il y avait alors trois stratégies pour la quadruple émission.
La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Les FPUs étaient soit dupliquées, soit scindées en deux en séparant l'additionneur du multiplieur flottant. Le POWER 2 dupliquait ses FPUs à la perfection, alors que l'21264 partitionnait la FPU. Les accès mémoire étaient traités comme des opérations entières, et le processeur pouvait : soit en faire un seul par cycle, soit en émettre deux, sous conditions (le cache devait être double port pour lire/écrire deux données à la fois, comme on le verra dans la suite du chapitre).
Les processeurs MIPS R8000 et R10000 allaient un peu plus loin, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités.
La seconde implémentation dupliquait l'ALU entière et combinait le tout avec un partitionnement INT-FLOAT-MEM. De quoi exécuter en même temps deux opérations entières, une opération flottante, et un accès mémoire. 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).
Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, avec au minimum deux ALU entières. Pour comprendre ce que je veux dire, prenons l'exemple de l'UltraSPARC. Il avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplier flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes. Le processeur pouvait émettre 4 instructions, tant qu'elles atterrissaient dans des unités différentes.
La même méthode peut être utilisée pour la double émission. Le Motorola 88110, sorti en 1991, était un CPU à double émission avec pas moins de 10 unités : deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise.
La morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes. Le tout est résumé dans le tableau ci-dessous, avec :
* en vert les CPU utilisant la première stratégie ;
* en rouge les CPU utilisant la seconde stratégie ;
* en jaune les CPU utilisant la troisième stratégie.
{|class="wikitable"
|+ Processeurs quadruple émission historiques
|-
! Année !! Processeur !! Unités
|-
! 1991
| class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques
|-
|-
! rowspan="2" | 1994
| class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'')
|-
| class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM
|-
! rowspan="3" | 1995
| class="f_jaune | PA 8000 || class="f_jaune | 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD
|-
| class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques
|-
| MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM
|-
! rowspan="2" | 1996
| class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM
|-
| class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL)
|}
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
tsxy9wea34e47grh1a1krrdusiofhs7
773187
773186
2026-09-25T20:33:41Z
Mewtow
31375
/* Les CPU superscalaires mélangent les trois techniques */
773187
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===L'impact des contraintes d’appariement sur la performance===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour la suite du chapitre===
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==Le ''front-end'' des processeurs superscalaires==
Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple.
Intuitivement, charger, décoder et 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 certaines processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. 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
|}
Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs.
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===L'émission parallèle des instructions===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elle sont propagée au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage).
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
==Les processeurs implémentés avec plusieurs pipelines==
Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Vous vous dites que l'implémentation d'un processeur superscalaire n'est pas la même suivant que le CPU utilise l'exécution dans le désordre, ou non. Mais dans les faits, la distinction est plus subtile que ca. Pour l'expliquer, je vais utiliser une classification originale, qui sépare les processeurs superscalaires en trois sous-types. La distinction se fait sur la manière dont la superscalarité est implémentée en matériel.La classification que j'utilise distingue :
* Les processeurs avec plusieurs pipelines, sans exécution dans le désordre.
* Les processeurs superscalaires avec un pipeline dynamique.
* Les processeurs avec des fenêtres d'instruction décentralisées.
La distinction n'est pas qu'une question d'implémentation matérielle. Elle impacte aussi le ratio entre instructions chargées/décodées et instructions émises. Vous vous dites que les deux sont égales, mais ce n'est le cas que sur les processeurs avec N pipelines ! Sur les autres processeurs, il est possible de décoder 4 instructions, mais d'en émettre maximum 6 par cycle ! Et sur les CPU avec des fenêtres d'instruction décentralisées, il y a aussi une différence entre le nombre d'instructions qui sont ''dispatch'' et ''schedule''.
La catégorie la plus simple est celle des '''processeurs avec plusieurs pipelines'''. Ce sont des processeurs superscalaires sans exécution dans le désordre. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction.
Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, d'une FPU, éventuellement d'un second banc de registres, guère plus. Il permet d'exécuter une seconde opération de calcul, pas plus, ce qui est à l'origine de contraintes d'appariemment assez fortes. De plus, il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section.
===La double émission entière-flottante===
L'exemple le plus simple utilise un pipeline pour les opérations flottantes, séparé du pipeline originel. Pour bien expliquer les choses, prenons le processeur illustré ci-dessous. Un processeur a deux ''back-end'' séparés, un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par l'ALU entière, passent éventuellement dans l'unité mémoire, pour ensuite arriver à l'étape de ''writeback''. La FPU et le banc de registre flottant ont eux aussi leur propre ''back-end'' séparé.
[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]]
Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaqaue pipeline, à chaque cycle.
L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine.
Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants.
===La double émission entière===
Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait.
Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
==Les processeurs superscalaires dynamiques==
Les processeurs précédents, avec 2 pipelines, avaient une unité d'émission assez simple. Elle avait deux ports de décodage, et deux ports d'émission. Le nombre de ports de décodage et d'émission étaient identiques. Vous vous dites que c'est naturel, et intuitif, que c'est normal pour un processeur superscalaire. Mais en réalité, la majorité des processeurs superscalaires a plus de ports d'émission que de décodage. Expliquer pourquoi va cependant nous demander de parler de comment la superscalarité exploite les unités de calcul.
Posons-nous cette question : de combien d'unités un processeur superscalaire a besoin ? Les unités en question ne sont nécessairement des unité de calcul, l'unité mémoire et l'unité de branchement comptent aussi. La réponse dépend du nombre de voies. Si le ''front-end'' charge et décode un paquet de N instructions, exécuter ces N instructions simultanément demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section.
La réponse dépend aussi des contraintes d’appariement. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission parfaite devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement, ce qui ferait passer de 6 unités à 12. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, le cout en circuit encore plus prohibitif !
En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée, alors que la double émission parait plus adaptée.
Et c'est là que la différence entre ports d'émission et de décodage devient intéressante. L'idée est d'avoir une unité d'émission avec deux ports de décodage, et 4 port d'émission (un par unité). Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux micro-opérations tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu.
Les tout premiers CPU superscalaires étaient nombreux à utiliser cette technique. Les exemples classiques sont l'HyperSPARC, de l'alpha 21064 et du PowerPC 603. C'étaient des CPU double émission, avec une ALU entière, une unité mémoire et une FPU. Nous parlerons de '''partitionnement INT-FLOAT-MEM'''. Les contraintes d’appariement variaient grandement suivant le processeur.
Le PowerPC 603 implémentait une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur.
Avant de poursuivre, précisons que la FPU regroupe un additionneur flottant et un multiplieur flottant. Et ce fait peut être exploité pour approfondir le partitionnement. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions).
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple, facile à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. La raison est que le partitionnement permet d'émettre des paires, triplets, quadruplets de micro-opérations qui sont rares en pratique.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''.
Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, 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. Et enfin, il faut rajouter un port d'émission pour pouvoir émettre une micro-opération entière en plus.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle.
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
Reste à connecter les unités de calcul à l'unité d'émission (indirectement, car il y a des étages de pipeline entre les deux). Une implémentation basique utilisee un port d'émission par unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La présence d'un circuit multiplieur et d'un ''barrel shifter'' pose des problèmes pour le partitionnement. Il est possible d'avoir un port d'émission dédié au multiplieur, et un autre au ''barrel shifter'', qui sont séparés de ceux pour l'ALU entière. Il est alors possible d'émettre une opération entière, une multiplication et un décalage, simultanément. Nous parlerons de '''partitionnement entier''' pour désigner cette possibilité.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
Le partitionnement entier est cependant très rare, car le gain en performance serait trop faible au regard des couts. Quelques processeurs historiques l'utilisaient, mais ce n'est plus le cas de nos jours. La raison est que les multiplications et les décalages sont assez rares dans le code, ce qui fait que le port d'émission associés sont sous-utilisés. Et un port d'émission a un cout en circuit assez important. 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.
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul. L'exemple classique est justement lié aux circuits multiplieur et ''barrel shifters''. L'idée est que le multiplieur et le ''barrel shifter'' sont mis sur le même port d'émission qu'une ALU entière.
Prenons par exemple le processeur à double émission entière illustré ci-contre. Un port d'émission est relié à une ALU entière seule, et l'autre port d'émission alimente la seconde ALU entière et le multiplieur. La conséquence est que le processeur peut émettre deux opérations entières, dont une multiplication. Si on avait utilisé un port d'émission dédié au multiplieur, il aurait été possible d'émettre deux opérations entières et une multiplication. On perd donc une voie, mais on gagne en flexibilité et on économise des circuits.
[[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]]
En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais le partage des ports d'émission peut appliquer à n'importe quelle unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. 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.
La solution est de partager des ports d'émission, pour passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple relie une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites. Le CPU peut émettre 6 opérations entières simultanément, grâce à 6 ALU entières. Les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires mélangent les trois techniques===
Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autre unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents.
* Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU.
* La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants.
* La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables.
* Le '''partage des ports d'émission''' réduit le cout en circuit du CPU, pour des pertes de performances mineures si bien fait.
De nos jours, le partitionnement est utilisé en complément de la duplication des ALU entières, de la duplication des autres unités, et du partage des ports d'émission. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. Le partage des ports d'émission a lui aussi mis un peu de temps pour s'imposer.
Les processeurs superscalaires anciens dupliquaient l'ALU et utilisaient beaucoup de ports d'émission. La seule contrainte est que les multipliuers et les ''barrel shifter'' partageaient leur port d'émission avec une ALU. Prenons l'exemple des 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
|}
Les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon, sont un exemple moins représentatif. Ces microarchitectures utilisent le partage des ports d'émission au maximum. C'était des microarchitectures à triple émission, avec trois ALU entières et trois unités mémoire, chaque ALU partageait un port d'émission avec une unité mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements.
Les CPU superscalaires tendent à mettre le paquet niveau opérations arithmétiques, mais pas forcément au point de remplir la totalité de la largeur du processeur. La règle est que pour N voies, le processeur dispose d'au moins N/2 ALU entières. Attention, cela ne signifie pas que le processeur émet systématiquement N/2 opération entières. Il peut par exemple émettre une combinaison d'opérations flottantes et mémoire, avec des branchements. Et N/2 est une borne basse, la plupart des CPU x86 ont environ tournent plutôt autour des 2/3, voire des 3/4.
En général, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. La conséquence est que les unités mémoire sont dupliquées, en seconde place dans l'ordre d'importance. Les processeurs x86 ont commencé à dupliquer leurs unités mémoire dès qu'ils ont atteint la quadruple émission, pour les CPU grand public. La microarchitecture Core d'Intel utilisait la quadruple émission, avec deux unités mémoire et trois ALU entières. Les microarchitectures d'AMD K5 et K6 utilisaient aussi la quadruple émission et avaient deux ALUs pour deux unités mémoire.
Plus récemment, la microarchitecture ''Golden Cove'' dispose de cinq unités de calcul trois unités mémoire (deux spécialisées dans les écritures, trois dans les lectures). La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire.Il y a donc soit égalité, soit avantage pour les ALU entières.
Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les CPU superscalaire à exécution dans le désordre==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Les unités d'exécution dans le désordre===
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports de ''dispatch''. Mais avec des stations de réservation, il faut le double des ports de ''schedule''. La raison est que les registres sont lus après émission avec une fenê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 de ''schedule'' que de ''dispatch''.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de 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===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Avec plusieurs dizaines d'unités de calcul différentes, ajouter autant 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.
La première solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Le processeur détecte quand il n'y a pas assez de ports pour servir toutes les micro-opérations. Quand ça arrive, l'unité d'émission émet assez de micro-opérations pour exploiter les ports disponibles, mais met en attente les micro-opérations en trop, le temps que les ports se libèrent. Pour cela, le banc de registre est géré par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Une autre solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une dernière solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
==L'évolution historique de la superscalarité==
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. La plupart des CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 sont rapidement montés à de la quadruple émission, avec quelques exceptions. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 ||
|-
! 1995
| 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000
|-
! 1996
| class="f_bleu | 620 || || || || class="f_bleu | 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
===Les processeurs historiques à double émission===
Pour commencer, étudions le cas des premiers CPU superscalaires à double émission. Ils sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Notamment, la duplication des autres unités ne sert pour ainsi dire à rien, ce qui limite les possibilités, mais rendra les explications plus simples.
Il pouvaient se classer en trois types principaux, avec quelques processeurs qui ne rentraient pas dans ces cases. Le premier type utilise un ''partitionnement complet'' entre ALU, FPU et unité mémoire. Le second utilisait la double émission entière-flottante. Le troisième utilisait la double émission entière. Voici le tableau chronologique disant quel CPU utilisait quelle technique.
* Le partitionnement INT-FLOAT-MEM est en jaune.
* La double émission entière-flottante est en vert.
* La double émission entière est en rouge.
* Les CPU barrés utilisent la quadruple émission et seront vu dans la section suivante.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| class="f_vert | RS/6000 || || || class="f_rouge | i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| class="f_vert | RSC || class="f_vert | PA 7100 || SuperSPARC || || class="f_jaune | 21064 ||
|-
! 1993
| class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || ||
|-
! 1994
| <s>POWER 2</s> || PA 7200 || || || <s>21164</s> ||
|}
Les processeurs laissés de côté dans le tableau précédent ne rentrent pas dans les catégories précédentes.
Le PA 7200 est un PA-7100 auquel on aurait rajouté une ALU entière. Le processeur exécutait les multiplications entières dans la FPU, comme son prédécesseur. Les combinaisons autorisées étaient donc INT + INT, INT + MEM, INT + FLOAT/MUL, MEM + FLOAT/MUL.
Le SuperSPARC était un CPU triple émission, avec deux ALU entières, une FPU et une unité mémoire. Il avait de nombreuses contraintes d’appariement. Par exemple, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Une bonne partie de ces contraintes était lié au banc de registre, qui n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions. Il faut noter que la seconde ALU a été abandonnée lors du passage au CPU HyperSPARC, qui est de plus repassé en double émission, ce qui est une évolution à rebours de l'évolution des autres CPUs.
===Les processeurs historiques à quadruple émission===
C'est la même année , en 1994, que la quadruple émission est arrivée et que les CPU ont commencé à avoir de plus en plus d'unités de calcul. Même les designs restés en double émission avaient ajouté des unités de calcul. Il y avait alors trois stratégies pour la quadruple émission.
La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Les FPUs étaient soit dupliquées, soit scindées en deux en séparant l'additionneur du multiplieur flottant. Le POWER 2 dupliquait ses FPUs à la perfection, alors que l'21264 partitionnait la FPU. Les accès mémoire étaient traités comme des opérations entières, et le processeur pouvait : soit en faire un seul par cycle, soit en émettre deux, sous conditions (le cache devait être double port pour lire/écrire deux données à la fois, comme on le verra dans la suite du chapitre).
Les processeurs MIPS R8000 et R10000 allaient un peu plus loin, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités.
La seconde implémentation dupliquait l'ALU entière et combinait le tout avec un partitionnement INT-FLOAT-MEM. De quoi exécuter en même temps deux opérations entières, une opération flottante, et un accès mémoire. 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).
Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, avec au minimum deux ALU entières. Pour comprendre ce que je veux dire, prenons l'exemple de l'UltraSPARC. Il avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplier flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes. Le processeur pouvait émettre 4 instructions, tant qu'elles atterrissaient dans des unités différentes.
La même méthode peut être utilisée pour la double émission. Le Motorola 88110, sorti en 1991, était un CPU à double émission avec pas moins de 10 unités : deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise.
La morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes. Le tout est résumé dans le tableau ci-dessous, avec :
* en vert les CPU utilisant la première stratégie ;
* en rouge les CPU utilisant la seconde stratégie ;
* en jaune les CPU utilisant la troisième stratégie.
{|class="wikitable"
|+ Processeurs quadruple émission historiques
|-
! Année !! Processeur !! Unités
|-
! 1991
| class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques
|-
|-
! rowspan="2" | 1994
| class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'')
|-
| class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM
|-
! rowspan="3" | 1995
| class="f_jaune | PA 8000 || class="f_jaune | 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD
|-
| class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques
|-
| MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM
|-
! rowspan="2" | 1996
| class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM
|-
| class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL)
|}
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
byxs8ppu3m25jmdc6vyzuv1lcfraujk
773188
773187
2026-09-25T20:36:26Z
Mewtow
31375
/* Les CPU superscalaires mélangent les trois techniques */
773188
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===L'impact des contraintes d’appariement sur la performance===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour la suite du chapitre===
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==Le ''front-end'' des processeurs superscalaires==
Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple.
Intuitivement, charger, décoder et 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 certaines processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. 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
|}
Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs.
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===L'émission parallèle des instructions===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elle sont propagée au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage).
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
==Les processeurs implémentés avec plusieurs pipelines==
Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Vous vous dites que l'implémentation d'un processeur superscalaire n'est pas la même suivant que le CPU utilise l'exécution dans le désordre, ou non. Mais dans les faits, la distinction est plus subtile que ca. Pour l'expliquer, je vais utiliser une classification originale, qui sépare les processeurs superscalaires en trois sous-types. La distinction se fait sur la manière dont la superscalarité est implémentée en matériel.La classification que j'utilise distingue :
* Les processeurs avec plusieurs pipelines, sans exécution dans le désordre.
* Les processeurs superscalaires avec un pipeline dynamique.
* Les processeurs avec des fenêtres d'instruction décentralisées.
La distinction n'est pas qu'une question d'implémentation matérielle. Elle impacte aussi le ratio entre instructions chargées/décodées et instructions émises. Vous vous dites que les deux sont égales, mais ce n'est le cas que sur les processeurs avec N pipelines ! Sur les autres processeurs, il est possible de décoder 4 instructions, mais d'en émettre maximum 6 par cycle ! Et sur les CPU avec des fenêtres d'instruction décentralisées, il y a aussi une différence entre le nombre d'instructions qui sont ''dispatch'' et ''schedule''.
La catégorie la plus simple est celle des '''processeurs avec plusieurs pipelines'''. Ce sont des processeurs superscalaires sans exécution dans le désordre. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction.
Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, d'une FPU, éventuellement d'un second banc de registres, guère plus. Il permet d'exécuter une seconde opération de calcul, pas plus, ce qui est à l'origine de contraintes d'appariemment assez fortes. De plus, il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section.
===La double émission entière-flottante===
L'exemple le plus simple utilise un pipeline pour les opérations flottantes, séparé du pipeline originel. Pour bien expliquer les choses, prenons le processeur illustré ci-dessous. Un processeur a deux ''back-end'' séparés, un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par l'ALU entière, passent éventuellement dans l'unité mémoire, pour ensuite arriver à l'étape de ''writeback''. La FPU et le banc de registre flottant ont eux aussi leur propre ''back-end'' séparé.
[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]]
Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaqaue pipeline, à chaque cycle.
L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine.
Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants.
===La double émission entière===
Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait.
Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
==Les processeurs superscalaires dynamiques==
Les processeurs précédents, avec 2 pipelines, avaient une unité d'émission assez simple. Elle avait deux ports de décodage, et deux ports d'émission. Le nombre de ports de décodage et d'émission étaient identiques. Vous vous dites que c'est naturel, et intuitif, que c'est normal pour un processeur superscalaire. Mais en réalité, la majorité des processeurs superscalaires a plus de ports d'émission que de décodage. Expliquer pourquoi va cependant nous demander de parler de comment la superscalarité exploite les unités de calcul.
Posons-nous cette question : de combien d'unités un processeur superscalaire a besoin ? Les unités en question ne sont nécessairement des unité de calcul, l'unité mémoire et l'unité de branchement comptent aussi. La réponse dépend du nombre de voies. Si le ''front-end'' charge et décode un paquet de N instructions, exécuter ces N instructions simultanément demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section.
La réponse dépend aussi des contraintes d’appariement. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission parfaite devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement, ce qui ferait passer de 6 unités à 12. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, le cout en circuit encore plus prohibitif !
En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée, alors que la double émission parait plus adaptée.
Et c'est là que la différence entre ports d'émission et de décodage devient intéressante. L'idée est d'avoir une unité d'émission avec deux ports de décodage, et 4 port d'émission (un par unité). Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux micro-opérations tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu.
Les tout premiers CPU superscalaires étaient nombreux à utiliser cette technique. Les exemples classiques sont l'HyperSPARC, de l'alpha 21064 et du PowerPC 603. C'étaient des CPU double émission, avec une ALU entière, une unité mémoire et une FPU. Nous parlerons de '''partitionnement INT-FLOAT-MEM'''. Les contraintes d’appariement variaient grandement suivant le processeur.
Le PowerPC 603 implémentait une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur.
Avant de poursuivre, précisons que la FPU regroupe un additionneur flottant et un multiplieur flottant. Et ce fait peut être exploité pour approfondir le partitionnement. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions).
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple, facile à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. La raison est que le partitionnement permet d'émettre des paires, triplets, quadruplets de micro-opérations qui sont rares en pratique.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''.
Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, 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. Et enfin, il faut rajouter un port d'émission pour pouvoir émettre une micro-opération entière en plus.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle.
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
Reste à connecter les unités de calcul à l'unité d'émission (indirectement, car il y a des étages de pipeline entre les deux). Une implémentation basique utilisee un port d'émission par unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La présence d'un circuit multiplieur et d'un ''barrel shifter'' pose des problèmes pour le partitionnement. Il est possible d'avoir un port d'émission dédié au multiplieur, et un autre au ''barrel shifter'', qui sont séparés de ceux pour l'ALU entière. Il est alors possible d'émettre une opération entière, une multiplication et un décalage, simultanément. Nous parlerons de '''partitionnement entier''' pour désigner cette possibilité.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
Le partitionnement entier est cependant très rare, car le gain en performance serait trop faible au regard des couts. Quelques processeurs historiques l'utilisaient, mais ce n'est plus le cas de nos jours. La raison est que les multiplications et les décalages sont assez rares dans le code, ce qui fait que le port d'émission associés sont sous-utilisés. Et un port d'émission a un cout en circuit assez important. 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.
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul. L'exemple classique est justement lié aux circuits multiplieur et ''barrel shifters''. L'idée est que le multiplieur et le ''barrel shifter'' sont mis sur le même port d'émission qu'une ALU entière.
Prenons par exemple le processeur à double émission entière illustré ci-contre. Un port d'émission est relié à une ALU entière seule, et l'autre port d'émission alimente la seconde ALU entière et le multiplieur. La conséquence est que le processeur peut émettre deux opérations entières, dont une multiplication. Si on avait utilisé un port d'émission dédié au multiplieur, il aurait été possible d'émettre deux opérations entières et une multiplication. On perd donc une voie, mais on gagne en flexibilité et on économise des circuits.
[[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]]
En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais le partage des ports d'émission peut appliquer à n'importe quelle unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. 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.
La solution est de partager des ports d'émission, pour passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple relie une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites. Le CPU peut émettre 6 opérations entières simultanément, grâce à 6 ALU entières. Les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires mélangent les trois techniques===
Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autre unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents.
* Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU.
* La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants.
* La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables.
* Le '''partage des ports d'émission''' réduit le cout en circuit du CPU, pour des pertes de performances mineures si bien fait.
De nos jours, le partitionnement est utilisé en complément de la duplication des ALU entières, de la duplication des autres unités, et du partage des ports d'émission. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. Le partage des ports d'émission a lui aussi mis un peu de temps pour s'imposer.
Les processeurs superscalaires anciens dupliquaient l'ALU et utilisaient beaucoup de ports d'émission. La seule contrainte est que les multiplieurs et les ''barrel shifter'' partageaient leur port d'émission avec une ALU. Prenons l'exemple des 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
|}
Les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon, sont un exemple moins représentatif. Ces microarchitectures utilisent le partage des ports d'émission au maximum. C'était des microarchitectures à triple émission, avec trois ALU entières et trois unités mémoire, chaque ALU partageait un port d'émission avec une unité mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements.
{|class="wikitable"
|-
! Port 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 || ALU entière || FADD || FMUL || FPU (autres opérations)
|-
| Multiplieur || || || || ||
|-
| LOAD/STORE || LOAD/STORE || LOAD/STORE || || ||
|}
Les CPU superscalaires tendent à mettre le paquet niveau opérations arithmétiques, mais pas forcément au point de remplir la totalité de la largeur du processeur. La règle est que pour N voies, le processeur dispose d'au moins N/2 ALU entières. Attention, cela ne signifie pas que le processeur émet systématiquement N/2 opération entières. Il peut par exemple émettre une combinaison d'opérations flottantes et mémoire, avec des branchements. Et N/2 est une borne basse, la plupart des CPU x86 ont environ tournent plutôt autour des 2/3, voire des 3/4.
En général, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. La conséquence est que les unités mémoire sont dupliquées, en seconde place dans l'ordre d'importance. Les processeurs x86 ont commencé à dupliquer leurs unités mémoire dès qu'ils ont atteint la quadruple émission, pour les CPU grand public. La microarchitecture Core d'Intel utilisait la quadruple émission, avec deux unités mémoire et trois ALU entières. Les microarchitectures d'AMD K5 et K6 utilisaient aussi la quadruple émission et avaient deux ALUs pour deux unités mémoire.
Plus récemment, la microarchitecture ''Golden Cove'' dispose de cinq unités de calcul trois unités mémoire (deux spécialisées dans les écritures, trois dans les lectures). La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire.Il y a donc soit égalité, soit avantage pour les ALU entières.
Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les CPU superscalaire à exécution dans le désordre==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Les unités d'exécution dans le désordre===
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports de ''dispatch''. Mais avec des stations de réservation, il faut le double des ports de ''schedule''. La raison est que les registres sont lus après émission avec une fenê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 de ''schedule'' que de ''dispatch''.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de 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===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Avec plusieurs dizaines d'unités de calcul différentes, ajouter autant 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.
La première solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Le processeur détecte quand il n'y a pas assez de ports pour servir toutes les micro-opérations. Quand ça arrive, l'unité d'émission émet assez de micro-opérations pour exploiter les ports disponibles, mais met en attente les micro-opérations en trop, le temps que les ports se libèrent. Pour cela, le banc de registre est géré par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Une autre solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une dernière solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
==L'évolution historique de la superscalarité==
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. La plupart des CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 sont rapidement montés à de la quadruple émission, avec quelques exceptions. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 ||
|-
! 1995
| 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000
|-
! 1996
| class="f_bleu | 620 || || || || class="f_bleu | 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
===Les processeurs historiques à double émission===
Pour commencer, étudions le cas des premiers CPU superscalaires à double émission. Ils sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Notamment, la duplication des autres unités ne sert pour ainsi dire à rien, ce qui limite les possibilités, mais rendra les explications plus simples.
Il pouvaient se classer en trois types principaux, avec quelques processeurs qui ne rentraient pas dans ces cases. Le premier type utilise un ''partitionnement complet'' entre ALU, FPU et unité mémoire. Le second utilisait la double émission entière-flottante. Le troisième utilisait la double émission entière. Voici le tableau chronologique disant quel CPU utilisait quelle technique.
* Le partitionnement INT-FLOAT-MEM est en jaune.
* La double émission entière-flottante est en vert.
* La double émission entière est en rouge.
* Les CPU barrés utilisent la quadruple émission et seront vu dans la section suivante.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| class="f_vert | RS/6000 || || || class="f_rouge | i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| class="f_vert | RSC || class="f_vert | PA 7100 || SuperSPARC || || class="f_jaune | 21064 ||
|-
! 1993
| class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || ||
|-
! 1994
| <s>POWER 2</s> || PA 7200 || || || <s>21164</s> ||
|}
Les processeurs laissés de côté dans le tableau précédent ne rentrent pas dans les catégories précédentes.
Le PA 7200 est un PA-7100 auquel on aurait rajouté une ALU entière. Le processeur exécutait les multiplications entières dans la FPU, comme son prédécesseur. Les combinaisons autorisées étaient donc INT + INT, INT + MEM, INT + FLOAT/MUL, MEM + FLOAT/MUL.
Le SuperSPARC était un CPU triple émission, avec deux ALU entières, une FPU et une unité mémoire. Il avait de nombreuses contraintes d’appariement. Par exemple, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Une bonne partie de ces contraintes était lié au banc de registre, qui n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions. Il faut noter que la seconde ALU a été abandonnée lors du passage au CPU HyperSPARC, qui est de plus repassé en double émission, ce qui est une évolution à rebours de l'évolution des autres CPUs.
===Les processeurs historiques à quadruple émission===
C'est la même année , en 1994, que la quadruple émission est arrivée et que les CPU ont commencé à avoir de plus en plus d'unités de calcul. Même les designs restés en double émission avaient ajouté des unités de calcul. Il y avait alors trois stratégies pour la quadruple émission.
La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Les FPUs étaient soit dupliquées, soit scindées en deux en séparant l'additionneur du multiplieur flottant. Le POWER 2 dupliquait ses FPUs à la perfection, alors que l'21264 partitionnait la FPU. Les accès mémoire étaient traités comme des opérations entières, et le processeur pouvait : soit en faire un seul par cycle, soit en émettre deux, sous conditions (le cache devait être double port pour lire/écrire deux données à la fois, comme on le verra dans la suite du chapitre).
Les processeurs MIPS R8000 et R10000 allaient un peu plus loin, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités.
La seconde implémentation dupliquait l'ALU entière et combinait le tout avec un partitionnement INT-FLOAT-MEM. De quoi exécuter en même temps deux opérations entières, une opération flottante, et un accès mémoire. 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).
Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, avec au minimum deux ALU entières. Pour comprendre ce que je veux dire, prenons l'exemple de l'UltraSPARC. Il avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplier flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes. Le processeur pouvait émettre 4 instructions, tant qu'elles atterrissaient dans des unités différentes.
La même méthode peut être utilisée pour la double émission. Le Motorola 88110, sorti en 1991, était un CPU à double émission avec pas moins de 10 unités : deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise.
La morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes. Le tout est résumé dans le tableau ci-dessous, avec :
* en vert les CPU utilisant la première stratégie ;
* en rouge les CPU utilisant la seconde stratégie ;
* en jaune les CPU utilisant la troisième stratégie.
{|class="wikitable"
|+ Processeurs quadruple émission historiques
|-
! Année !! Processeur !! Unités
|-
! 1991
| class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques
|-
|-
! rowspan="2" | 1994
| class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'')
|-
| class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM
|-
! rowspan="3" | 1995
| class="f_jaune | PA 8000 || class="f_jaune | 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD
|-
| class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques
|-
| MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM
|-
! rowspan="2" | 1996
| class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM
|-
| class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL)
|}
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
penfmz9ljbnx12nuif8p53edqqiq9uo
773189
773188
2026-09-25T20:37:33Z
Mewtow
31375
/* Les CPU superscalaires mélangent les trois techniques */
773189
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===L'impact des contraintes d’appariement sur la performance===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour la suite du chapitre===
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==Le ''front-end'' des processeurs superscalaires==
Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple.
Intuitivement, charger, décoder et 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 certaines processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. 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
|}
Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs.
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===L'émission parallèle des instructions===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elle sont propagée au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage).
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
==Les processeurs implémentés avec plusieurs pipelines==
Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Vous vous dites que l'implémentation d'un processeur superscalaire n'est pas la même suivant que le CPU utilise l'exécution dans le désordre, ou non. Mais dans les faits, la distinction est plus subtile que ca. Pour l'expliquer, je vais utiliser une classification originale, qui sépare les processeurs superscalaires en trois sous-types. La distinction se fait sur la manière dont la superscalarité est implémentée en matériel.La classification que j'utilise distingue :
* Les processeurs avec plusieurs pipelines, sans exécution dans le désordre.
* Les processeurs superscalaires avec un pipeline dynamique.
* Les processeurs avec des fenêtres d'instruction décentralisées.
La distinction n'est pas qu'une question d'implémentation matérielle. Elle impacte aussi le ratio entre instructions chargées/décodées et instructions émises. Vous vous dites que les deux sont égales, mais ce n'est le cas que sur les processeurs avec N pipelines ! Sur les autres processeurs, il est possible de décoder 4 instructions, mais d'en émettre maximum 6 par cycle ! Et sur les CPU avec des fenêtres d'instruction décentralisées, il y a aussi une différence entre le nombre d'instructions qui sont ''dispatch'' et ''schedule''.
La catégorie la plus simple est celle des '''processeurs avec plusieurs pipelines'''. Ce sont des processeurs superscalaires sans exécution dans le désordre. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction.
Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, d'une FPU, éventuellement d'un second banc de registres, guère plus. Il permet d'exécuter une seconde opération de calcul, pas plus, ce qui est à l'origine de contraintes d'appariemment assez fortes. De plus, il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section.
===La double émission entière-flottante===
L'exemple le plus simple utilise un pipeline pour les opérations flottantes, séparé du pipeline originel. Pour bien expliquer les choses, prenons le processeur illustré ci-dessous. Un processeur a deux ''back-end'' séparés, un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par l'ALU entière, passent éventuellement dans l'unité mémoire, pour ensuite arriver à l'étape de ''writeback''. La FPU et le banc de registre flottant ont eux aussi leur propre ''back-end'' séparé.
[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]]
Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaqaue pipeline, à chaque cycle.
L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine.
Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants.
===La double émission entière===
Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait.
Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
==Les processeurs superscalaires dynamiques==
Les processeurs précédents, avec 2 pipelines, avaient une unité d'émission assez simple. Elle avait deux ports de décodage, et deux ports d'émission. Le nombre de ports de décodage et d'émission étaient identiques. Vous vous dites que c'est naturel, et intuitif, que c'est normal pour un processeur superscalaire. Mais en réalité, la majorité des processeurs superscalaires a plus de ports d'émission que de décodage. Expliquer pourquoi va cependant nous demander de parler de comment la superscalarité exploite les unités de calcul.
Posons-nous cette question : de combien d'unités un processeur superscalaire a besoin ? Les unités en question ne sont nécessairement des unité de calcul, l'unité mémoire et l'unité de branchement comptent aussi. La réponse dépend du nombre de voies. Si le ''front-end'' charge et décode un paquet de N instructions, exécuter ces N instructions simultanément demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section.
La réponse dépend aussi des contraintes d’appariement. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission parfaite devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement, ce qui ferait passer de 6 unités à 12. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, le cout en circuit encore plus prohibitif !
En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée, alors que la double émission parait plus adaptée.
Et c'est là que la différence entre ports d'émission et de décodage devient intéressante. L'idée est d'avoir une unité d'émission avec deux ports de décodage, et 4 port d'émission (un par unité). Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux micro-opérations tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu.
Les tout premiers CPU superscalaires étaient nombreux à utiliser cette technique. Les exemples classiques sont l'HyperSPARC, de l'alpha 21064 et du PowerPC 603. C'étaient des CPU double émission, avec une ALU entière, une unité mémoire et une FPU. Nous parlerons de '''partitionnement INT-FLOAT-MEM'''. Les contraintes d’appariement variaient grandement suivant le processeur.
Le PowerPC 603 implémentait une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur.
Avant de poursuivre, précisons que la FPU regroupe un additionneur flottant et un multiplieur flottant. Et ce fait peut être exploité pour approfondir le partitionnement. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions).
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple, facile à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. La raison est que le partitionnement permet d'émettre des paires, triplets, quadruplets de micro-opérations qui sont rares en pratique.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''.
Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, 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. Et enfin, il faut rajouter un port d'émission pour pouvoir émettre une micro-opération entière en plus.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle.
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
Reste à connecter les unités de calcul à l'unité d'émission (indirectement, car il y a des étages de pipeline entre les deux). Une implémentation basique utilisee un port d'émission par unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La présence d'un circuit multiplieur et d'un ''barrel shifter'' pose des problèmes pour le partitionnement. Il est possible d'avoir un port d'émission dédié au multiplieur, et un autre au ''barrel shifter'', qui sont séparés de ceux pour l'ALU entière. Il est alors possible d'émettre une opération entière, une multiplication et un décalage, simultanément. Nous parlerons de '''partitionnement entier''' pour désigner cette possibilité.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
Le partitionnement entier est cependant très rare, car le gain en performance serait trop faible au regard des couts. Quelques processeurs historiques l'utilisaient, mais ce n'est plus le cas de nos jours. La raison est que les multiplications et les décalages sont assez rares dans le code, ce qui fait que le port d'émission associés sont sous-utilisés. Et un port d'émission a un cout en circuit assez important. 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.
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul. L'exemple classique est justement lié aux circuits multiplieur et ''barrel shifters''. L'idée est que le multiplieur et le ''barrel shifter'' sont mis sur le même port d'émission qu'une ALU entière.
Prenons par exemple le processeur à double émission entière illustré ci-contre. Un port d'émission est relié à une ALU entière seule, et l'autre port d'émission alimente la seconde ALU entière et le multiplieur. La conséquence est que le processeur peut émettre deux opérations entières, dont une multiplication. Si on avait utilisé un port d'émission dédié au multiplieur, il aurait été possible d'émettre deux opérations entières et une multiplication. On perd donc une voie, mais on gagne en flexibilité et on économise des circuits.
[[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]]
En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais le partage des ports d'émission peut appliquer à n'importe quelle unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. 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.
La solution est de partager des ports d'émission, pour passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple relie une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites. Le CPU peut émettre 6 opérations entières simultanément, grâce à 6 ALU entières. Les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires mélangent les trois techniques===
Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autre unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents.
* Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU.
* La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants.
* La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables.
* Le '''partage des ports d'émission''' réduit le cout en circuit du CPU, pour des pertes de performances mineures si bien fait.
De nos jours, le partitionnement est utilisé en complément de la duplication des ALU entières, de la duplication des autres unités, et du partage des ports d'émission. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. Le partage des ports d'émission a lui aussi mis un peu de temps pour s'imposer.
Les processeurs superscalaires anciens dupliquaient l'ALU et utilisaient beaucoup de ports d'émission. La seule contrainte est que les multiplieurs et les ''barrel shifter'' partageaient leur port d'émission avec une ALU. Prenons l'exemple des 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
|}
Les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon, sont un exemple moins représentatif. Ces microarchitectures utilisent le partage des ports d'émission au maximum. C'était des microarchitectures à triple émission, avec six ports d’émission. Il y avait trois prots d'émission pour les opérations flottantes, et trois ports d'émission pour les micro-opérations entières/mémoire. Il y avait trois ALU entières et trois unités mémoire, chaque ALU partageait un port d'émission avec une unité mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements.
{|class="wikitable"
|-
! Port 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 || ALU entière || FADD || FMUL || FPU (autres opérations)
|-
| Multiplieur || || || || ||
|-
| LOAD/STORE || LOAD/STORE || LOAD/STORE || || ||
|}
Les CPU superscalaires tendent à mettre le paquet niveau opérations arithmétiques, mais pas forcément au point de remplir la totalité de la largeur du processeur. La règle est que pour N voies, le processeur dispose d'au moins N/2 ALU entières. Attention, cela ne signifie pas que le processeur émet systématiquement N/2 opération entières. Il peut par exemple émettre une combinaison d'opérations flottantes et mémoire, avec des branchements. Et N/2 est une borne basse, la plupart des CPU x86 ont environ tournent plutôt autour des 2/3, voire des 3/4.
En général, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. La conséquence est que les unités mémoire sont dupliquées, en seconde place dans l'ordre d'importance. Les processeurs x86 ont commencé à dupliquer leurs unités mémoire dès qu'ils ont atteint la quadruple émission, pour les CPU grand public. La microarchitecture Core d'Intel utilisait la quadruple émission, avec deux unités mémoire et trois ALU entières. Les microarchitectures d'AMD K5 et K6 utilisaient aussi la quadruple émission et avaient deux ALUs pour deux unités mémoire.
Plus récemment, la microarchitecture ''Golden Cove'' dispose de cinq unités de calcul trois unités mémoire (deux spécialisées dans les écritures, trois dans les lectures). La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire.Il y a donc soit égalité, soit avantage pour les ALU entières.
Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les CPU superscalaire à exécution dans le désordre==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Les unités d'exécution dans le désordre===
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports de ''dispatch''. Mais avec des stations de réservation, il faut le double des ports de ''schedule''. La raison est que les registres sont lus après émission avec une fenê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 de ''schedule'' que de ''dispatch''.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de 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===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Avec plusieurs dizaines d'unités de calcul différentes, ajouter autant 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.
La première solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Le processeur détecte quand il n'y a pas assez de ports pour servir toutes les micro-opérations. Quand ça arrive, l'unité d'émission émet assez de micro-opérations pour exploiter les ports disponibles, mais met en attente les micro-opérations en trop, le temps que les ports se libèrent. Pour cela, le banc de registre est géré par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Une autre solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une dernière solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
==L'évolution historique de la superscalarité==
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. La plupart des CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 sont rapidement montés à de la quadruple émission, avec quelques exceptions. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 ||
|-
! 1995
| 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000
|-
! 1996
| class="f_bleu | 620 || || || || class="f_bleu | 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
===Les processeurs historiques à double émission===
Pour commencer, étudions le cas des premiers CPU superscalaires à double émission. Ils sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Notamment, la duplication des autres unités ne sert pour ainsi dire à rien, ce qui limite les possibilités, mais rendra les explications plus simples.
Il pouvaient se classer en trois types principaux, avec quelques processeurs qui ne rentraient pas dans ces cases. Le premier type utilise un ''partitionnement complet'' entre ALU, FPU et unité mémoire. Le second utilisait la double émission entière-flottante. Le troisième utilisait la double émission entière. Voici le tableau chronologique disant quel CPU utilisait quelle technique.
* Le partitionnement INT-FLOAT-MEM est en jaune.
* La double émission entière-flottante est en vert.
* La double émission entière est en rouge.
* Les CPU barrés utilisent la quadruple émission et seront vu dans la section suivante.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| class="f_vert | RS/6000 || || || class="f_rouge | i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| class="f_vert | RSC || class="f_vert | PA 7100 || SuperSPARC || || class="f_jaune | 21064 ||
|-
! 1993
| class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || ||
|-
! 1994
| <s>POWER 2</s> || PA 7200 || || || <s>21164</s> ||
|}
Les processeurs laissés de côté dans le tableau précédent ne rentrent pas dans les catégories précédentes.
Le PA 7200 est un PA-7100 auquel on aurait rajouté une ALU entière. Le processeur exécutait les multiplications entières dans la FPU, comme son prédécesseur. Les combinaisons autorisées étaient donc INT + INT, INT + MEM, INT + FLOAT/MUL, MEM + FLOAT/MUL.
Le SuperSPARC était un CPU triple émission, avec deux ALU entières, une FPU et une unité mémoire. Il avait de nombreuses contraintes d’appariement. Par exemple, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Une bonne partie de ces contraintes était lié au banc de registre, qui n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions. Il faut noter que la seconde ALU a été abandonnée lors du passage au CPU HyperSPARC, qui est de plus repassé en double émission, ce qui est une évolution à rebours de l'évolution des autres CPUs.
===Les processeurs historiques à quadruple émission===
C'est la même année , en 1994, que la quadruple émission est arrivée et que les CPU ont commencé à avoir de plus en plus d'unités de calcul. Même les designs restés en double émission avaient ajouté des unités de calcul. Il y avait alors trois stratégies pour la quadruple émission.
La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Les FPUs étaient soit dupliquées, soit scindées en deux en séparant l'additionneur du multiplieur flottant. Le POWER 2 dupliquait ses FPUs à la perfection, alors que l'21264 partitionnait la FPU. Les accès mémoire étaient traités comme des opérations entières, et le processeur pouvait : soit en faire un seul par cycle, soit en émettre deux, sous conditions (le cache devait être double port pour lire/écrire deux données à la fois, comme on le verra dans la suite du chapitre).
Les processeurs MIPS R8000 et R10000 allaient un peu plus loin, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités.
La seconde implémentation dupliquait l'ALU entière et combinait le tout avec un partitionnement INT-FLOAT-MEM. De quoi exécuter en même temps deux opérations entières, une opération flottante, et un accès mémoire. 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).
Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, avec au minimum deux ALU entières. Pour comprendre ce que je veux dire, prenons l'exemple de l'UltraSPARC. Il avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplier flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes. Le processeur pouvait émettre 4 instructions, tant qu'elles atterrissaient dans des unités différentes.
La même méthode peut être utilisée pour la double émission. Le Motorola 88110, sorti en 1991, était un CPU à double émission avec pas moins de 10 unités : deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise.
La morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes. Le tout est résumé dans le tableau ci-dessous, avec :
* en vert les CPU utilisant la première stratégie ;
* en rouge les CPU utilisant la seconde stratégie ;
* en jaune les CPU utilisant la troisième stratégie.
{|class="wikitable"
|+ Processeurs quadruple émission historiques
|-
! Année !! Processeur !! Unités
|-
! 1991
| class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques
|-
|-
! rowspan="2" | 1994
| class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'')
|-
| class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM
|-
! rowspan="3" | 1995
| class="f_jaune | PA 8000 || class="f_jaune | 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD
|-
| class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques
|-
| MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM
|-
! rowspan="2" | 1996
| class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM
|-
| class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL)
|}
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
axo9vatzyotnmq4tvy8ytcdkdsc0q2m
773190
773189
2026-09-25T20:42:53Z
Mewtow
31375
/* Les CPU superscalaires mélangent les trois techniques */
773190
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===L'impact des contraintes d’appariement sur la performance===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour la suite du chapitre===
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==Le ''front-end'' des processeurs superscalaires==
Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple.
Intuitivement, charger, décoder et 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 certaines processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. 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
|}
Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs.
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===L'émission parallèle des instructions===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elle sont propagée au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage).
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
==Les processeurs implémentés avec plusieurs pipelines==
Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Vous vous dites que l'implémentation d'un processeur superscalaire n'est pas la même suivant que le CPU utilise l'exécution dans le désordre, ou non. Mais dans les faits, la distinction est plus subtile que ca. Pour l'expliquer, je vais utiliser une classification originale, qui sépare les processeurs superscalaires en trois sous-types. La distinction se fait sur la manière dont la superscalarité est implémentée en matériel.La classification que j'utilise distingue :
* Les processeurs avec plusieurs pipelines, sans exécution dans le désordre.
* Les processeurs superscalaires avec un pipeline dynamique.
* Les processeurs avec des fenêtres d'instruction décentralisées.
La distinction n'est pas qu'une question d'implémentation matérielle. Elle impacte aussi le ratio entre instructions chargées/décodées et instructions émises. Vous vous dites que les deux sont égales, mais ce n'est le cas que sur les processeurs avec N pipelines ! Sur les autres processeurs, il est possible de décoder 4 instructions, mais d'en émettre maximum 6 par cycle ! Et sur les CPU avec des fenêtres d'instruction décentralisées, il y a aussi une différence entre le nombre d'instructions qui sont ''dispatch'' et ''schedule''.
La catégorie la plus simple est celle des '''processeurs avec plusieurs pipelines'''. Ce sont des processeurs superscalaires sans exécution dans le désordre. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction.
Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, d'une FPU, éventuellement d'un second banc de registres, guère plus. Il permet d'exécuter une seconde opération de calcul, pas plus, ce qui est à l'origine de contraintes d'appariemment assez fortes. De plus, il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section.
===La double émission entière-flottante===
L'exemple le plus simple utilise un pipeline pour les opérations flottantes, séparé du pipeline originel. Pour bien expliquer les choses, prenons le processeur illustré ci-dessous. Un processeur a deux ''back-end'' séparés, un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par l'ALU entière, passent éventuellement dans l'unité mémoire, pour ensuite arriver à l'étape de ''writeback''. La FPU et le banc de registre flottant ont eux aussi leur propre ''back-end'' séparé.
[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]]
Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaqaue pipeline, à chaque cycle.
L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine.
Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants.
===La double émission entière===
Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait.
Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
==Les processeurs superscalaires dynamiques==
Les processeurs précédents, avec 2 pipelines, avaient une unité d'émission assez simple. Elle avait deux ports de décodage, et deux ports d'émission. Le nombre de ports de décodage et d'émission étaient identiques. Vous vous dites que c'est naturel, et intuitif, que c'est normal pour un processeur superscalaire. Mais en réalité, la majorité des processeurs superscalaires a plus de ports d'émission que de décodage. Expliquer pourquoi va cependant nous demander de parler de comment la superscalarité exploite les unités de calcul.
Posons-nous cette question : de combien d'unités un processeur superscalaire a besoin ? Les unités en question ne sont nécessairement des unité de calcul, l'unité mémoire et l'unité de branchement comptent aussi. La réponse dépend du nombre de voies. Si le ''front-end'' charge et décode un paquet de N instructions, exécuter ces N instructions simultanément demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section.
La réponse dépend aussi des contraintes d’appariement. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission parfaite devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement, ce qui ferait passer de 6 unités à 12. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, le cout en circuit encore plus prohibitif !
En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée, alors que la double émission parait plus adaptée.
Et c'est là que la différence entre ports d'émission et de décodage devient intéressante. L'idée est d'avoir une unité d'émission avec deux ports de décodage, et 4 port d'émission (un par unité). Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux micro-opérations tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu.
Les tout premiers CPU superscalaires étaient nombreux à utiliser cette technique. Les exemples classiques sont l'HyperSPARC, de l'alpha 21064 et du PowerPC 603. C'étaient des CPU double émission, avec une ALU entière, une unité mémoire et une FPU. Nous parlerons de '''partitionnement INT-FLOAT-MEM'''. Les contraintes d’appariement variaient grandement suivant le processeur.
Le PowerPC 603 implémentait une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur.
Avant de poursuivre, précisons que la FPU regroupe un additionneur flottant et un multiplieur flottant. Et ce fait peut être exploité pour approfondir le partitionnement. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions).
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple, facile à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. La raison est que le partitionnement permet d'émettre des paires, triplets, quadruplets de micro-opérations qui sont rares en pratique.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''.
Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, 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. Et enfin, il faut rajouter un port d'émission pour pouvoir émettre une micro-opération entière en plus.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle.
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
Reste à connecter les unités de calcul à l'unité d'émission (indirectement, car il y a des étages de pipeline entre les deux). Une implémentation basique utilisee un port d'émission par unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La présence d'un circuit multiplieur et d'un ''barrel shifter'' pose des problèmes pour le partitionnement. Il est possible d'avoir un port d'émission dédié au multiplieur, et un autre au ''barrel shifter'', qui sont séparés de ceux pour l'ALU entière. Il est alors possible d'émettre une opération entière, une multiplication et un décalage, simultanément. Nous parlerons de '''partitionnement entier''' pour désigner cette possibilité.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
Le partitionnement entier est cependant très rare, car le gain en performance serait trop faible au regard des couts. Quelques processeurs historiques l'utilisaient, mais ce n'est plus le cas de nos jours. La raison est que les multiplications et les décalages sont assez rares dans le code, ce qui fait que le port d'émission associés sont sous-utilisés. Et un port d'émission a un cout en circuit assez important. 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.
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul. L'exemple classique est justement lié aux circuits multiplieur et ''barrel shifters''. L'idée est que le multiplieur et le ''barrel shifter'' sont mis sur le même port d'émission qu'une ALU entière.
Prenons par exemple le processeur à double émission entière illustré ci-contre. Un port d'émission est relié à une ALU entière seule, et l'autre port d'émission alimente la seconde ALU entière et le multiplieur. La conséquence est que le processeur peut émettre deux opérations entières, dont une multiplication. Si on avait utilisé un port d'émission dédié au multiplieur, il aurait été possible d'émettre deux opérations entières et une multiplication. On perd donc une voie, mais on gagne en flexibilité et on économise des circuits.
[[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]]
En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais le partage des ports d'émission peut appliquer à n'importe quelle unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. 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.
La solution est de partager des ports d'émission, pour passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple relie une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites. Le CPU peut émettre 6 opérations entières simultanément, grâce à 6 ALU entières. Les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires mélangent les trois techniques===
Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autre unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents.
* Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU.
* La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants.
* La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables.
* Le '''partage des ports d'émission''' réduit le cout en circuit du CPU, pour des pertes de performances mineures si bien fait.
De nos jours, le partitionnement est utilisé en complément de la duplication des ALU entières, de la duplication des autres unités, et du partage des ports d'émission. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. Le partage des ports d'émission a lui aussi mis un peu de temps pour s'imposer.
Les CPU superscalaires tendent à mettre le paquet niveau opérations arithmétiques, mais pas forcément au point de remplir la totalité de la largeur du processeur. La règle est que pour N voies, le processeur dispose d'au moins N/2 ALU entières. Attention, cela ne signifie pas que le processeur émet systématiquement N/2 opération entières. Il peut par exemple émettre une combinaison d'opérations flottantes et mémoire, avec des branchements. Et N/2 est une borne basse, la plupart des CPU x86 ont environ tournent plutôt autour des 2/3, voire des 3/4.
En général, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. La conséquence est que les unités mémoire sont dupliquées, en seconde place dans l'ordre d'importance. Les processeurs x86 ont commencé à dupliquer leurs unités mémoire dès qu'ils ont atteint la quadruple émission, pour les CPU grand public. Par exemple, la microarchitecture ''Golden Cove'' d'Intel dispose de cinq unités de calcul, trois unités mémoire (deux spécialisées dans les écritures, trois dans les lectures). La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire. Il y a donc soit égalité, soit avantage pour les ALU entières.
Prenons l'exemple des 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
|}
Les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon, sont un exemple moins représentatif. Ces microarchitectures utilisent le partage des ports d'émission au maximum. C'était des microarchitectures à triple émission, avec six ports d’émission. Il y avait trois prots d'émission pour les opérations flottantes, et trois ports d'émission pour les micro-opérations entières/mémoire. Il y avait trois ALU entières et trois unités mémoire, chaque ALU partageait un port d'émission avec une unité mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements.
{|class="wikitable"
|-
! Port 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 || ALU entière || FADD || FMUL || FPU (autres opérations)
|-
| Multiplieur || || || || ||
|-
| LOAD/STORE || LOAD/STORE || LOAD/STORE || || ||
|}
Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair.
Par exemple, la microarchitecture ''Golden Cove'' a deux ''barrel shifters''.
{|class="wikitable"
|-
! Port 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 !! Port d'émission n°7 !! Port d'émission n°8 !! Port d'émission n°9 !! Port d'émission n°10 !
|-
| ALU entière || ALU entière || ALU entière || ALU entière || ALU entière || LOAD || LOAD || LOAD || STORE || STORE
|-
| ''Barrel shifter'' || Multiplieur || Multiplieur || ''Barrel shifter'' || ||
|-
| Branchements || Diviseur || || Branchements ||
|}
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les CPU superscalaire à exécution dans le désordre==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Les unités d'exécution dans le désordre===
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports de ''dispatch''. Mais avec des stations de réservation, il faut le double des ports de ''schedule''. La raison est que les registres sont lus après émission avec une fenê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 de ''schedule'' que de ''dispatch''.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de 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===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Avec plusieurs dizaines d'unités de calcul différentes, ajouter autant 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.
La première solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Le processeur détecte quand il n'y a pas assez de ports pour servir toutes les micro-opérations. Quand ça arrive, l'unité d'émission émet assez de micro-opérations pour exploiter les ports disponibles, mais met en attente les micro-opérations en trop, le temps que les ports se libèrent. Pour cela, le banc de registre est géré par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Une autre solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une dernière solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
==L'évolution historique de la superscalarité==
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. La plupart des CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 sont rapidement montés à de la quadruple émission, avec quelques exceptions. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 ||
|-
! 1995
| 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000
|-
! 1996
| class="f_bleu | 620 || || || || class="f_bleu | 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
===Les processeurs historiques à double émission===
Pour commencer, étudions le cas des premiers CPU superscalaires à double émission. Ils sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Notamment, la duplication des autres unités ne sert pour ainsi dire à rien, ce qui limite les possibilités, mais rendra les explications plus simples.
Il pouvaient se classer en trois types principaux, avec quelques processeurs qui ne rentraient pas dans ces cases. Le premier type utilise un ''partitionnement complet'' entre ALU, FPU et unité mémoire. Le second utilisait la double émission entière-flottante. Le troisième utilisait la double émission entière. Voici le tableau chronologique disant quel CPU utilisait quelle technique.
* Le partitionnement INT-FLOAT-MEM est en jaune.
* La double émission entière-flottante est en vert.
* La double émission entière est en rouge.
* Les CPU barrés utilisent la quadruple émission et seront vu dans la section suivante.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| class="f_vert | RS/6000 || || || class="f_rouge | i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| class="f_vert | RSC || class="f_vert | PA 7100 || SuperSPARC || || class="f_jaune | 21064 ||
|-
! 1993
| class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || ||
|-
! 1994
| <s>POWER 2</s> || PA 7200 || || || <s>21164</s> ||
|}
Les processeurs laissés de côté dans le tableau précédent ne rentrent pas dans les catégories précédentes.
Le PA 7200 est un PA-7100 auquel on aurait rajouté une ALU entière. Le processeur exécutait les multiplications entières dans la FPU, comme son prédécesseur. Les combinaisons autorisées étaient donc INT + INT, INT + MEM, INT + FLOAT/MUL, MEM + FLOAT/MUL.
Le SuperSPARC était un CPU triple émission, avec deux ALU entières, une FPU et une unité mémoire. Il avait de nombreuses contraintes d’appariement. Par exemple, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Une bonne partie de ces contraintes était lié au banc de registre, qui n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions. Il faut noter que la seconde ALU a été abandonnée lors du passage au CPU HyperSPARC, qui est de plus repassé en double émission, ce qui est une évolution à rebours de l'évolution des autres CPUs.
===Les processeurs historiques à quadruple émission===
C'est la même année , en 1994, que la quadruple émission est arrivée et que les CPU ont commencé à avoir de plus en plus d'unités de calcul. Même les designs restés en double émission avaient ajouté des unités de calcul. Il y avait alors trois stratégies pour la quadruple émission.
La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Les FPUs étaient soit dupliquées, soit scindées en deux en séparant l'additionneur du multiplieur flottant. Le POWER 2 dupliquait ses FPUs à la perfection, alors que l'21264 partitionnait la FPU. Les accès mémoire étaient traités comme des opérations entières, et le processeur pouvait : soit en faire un seul par cycle, soit en émettre deux, sous conditions (le cache devait être double port pour lire/écrire deux données à la fois, comme on le verra dans la suite du chapitre).
Les processeurs MIPS R8000 et R10000 allaient un peu plus loin, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités.
La seconde implémentation dupliquait l'ALU entière et combinait le tout avec un partitionnement INT-FLOAT-MEM. De quoi exécuter en même temps deux opérations entières, une opération flottante, et un accès mémoire. 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).
Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, avec au minimum deux ALU entières. Pour comprendre ce que je veux dire, prenons l'exemple de l'UltraSPARC. Il avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplier flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes. Le processeur pouvait émettre 4 instructions, tant qu'elles atterrissaient dans des unités différentes.
La même méthode peut être utilisée pour la double émission. Le Motorola 88110, sorti en 1991, était un CPU à double émission avec pas moins de 10 unités : deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise.
La morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes. Le tout est résumé dans le tableau ci-dessous, avec :
* en vert les CPU utilisant la première stratégie ;
* en rouge les CPU utilisant la seconde stratégie ;
* en jaune les CPU utilisant la troisième stratégie.
{|class="wikitable"
|+ Processeurs quadruple émission historiques
|-
! Année !! Processeur !! Unités
|-
! 1991
| class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques
|-
|-
! rowspan="2" | 1994
| class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'')
|-
| class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM
|-
! rowspan="3" | 1995
| class="f_jaune | PA 8000 || class="f_jaune | 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD
|-
| class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques
|-
| MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM
|-
! rowspan="2" | 1996
| class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM
|-
| class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL)
|}
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
aek16c1ej1n80tg9rcen44m9i5uer0f
773191
773190
2026-09-25T20:44:09Z
Mewtow
31375
/* Les CPU superscalaires mélangent les trois techniques */
773191
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===L'impact des contraintes d’appariement sur la performance===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour la suite du chapitre===
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==Le ''front-end'' des processeurs superscalaires==
Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple.
Intuitivement, charger, décoder et 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 certaines processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. 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
|}
Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs.
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===L'émission parallèle des instructions===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elle sont propagée au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage).
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
==Les processeurs implémentés avec plusieurs pipelines==
Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Vous vous dites que l'implémentation d'un processeur superscalaire n'est pas la même suivant que le CPU utilise l'exécution dans le désordre, ou non. Mais dans les faits, la distinction est plus subtile que ca. Pour l'expliquer, je vais utiliser une classification originale, qui sépare les processeurs superscalaires en trois sous-types. La distinction se fait sur la manière dont la superscalarité est implémentée en matériel.La classification que j'utilise distingue :
* Les processeurs avec plusieurs pipelines, sans exécution dans le désordre.
* Les processeurs superscalaires avec un pipeline dynamique.
* Les processeurs avec des fenêtres d'instruction décentralisées.
La distinction n'est pas qu'une question d'implémentation matérielle. Elle impacte aussi le ratio entre instructions chargées/décodées et instructions émises. Vous vous dites que les deux sont égales, mais ce n'est le cas que sur les processeurs avec N pipelines ! Sur les autres processeurs, il est possible de décoder 4 instructions, mais d'en émettre maximum 6 par cycle ! Et sur les CPU avec des fenêtres d'instruction décentralisées, il y a aussi une différence entre le nombre d'instructions qui sont ''dispatch'' et ''schedule''.
La catégorie la plus simple est celle des '''processeurs avec plusieurs pipelines'''. Ce sont des processeurs superscalaires sans exécution dans le désordre. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction.
Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, d'une FPU, éventuellement d'un second banc de registres, guère plus. Il permet d'exécuter une seconde opération de calcul, pas plus, ce qui est à l'origine de contraintes d'appariemment assez fortes. De plus, il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section.
===La double émission entière-flottante===
L'exemple le plus simple utilise un pipeline pour les opérations flottantes, séparé du pipeline originel. Pour bien expliquer les choses, prenons le processeur illustré ci-dessous. Un processeur a deux ''back-end'' séparés, un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par l'ALU entière, passent éventuellement dans l'unité mémoire, pour ensuite arriver à l'étape de ''writeback''. La FPU et le banc de registre flottant ont eux aussi leur propre ''back-end'' séparé.
[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]]
Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaqaue pipeline, à chaque cycle.
L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine.
Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants.
===La double émission entière===
Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait.
Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
==Les processeurs superscalaires dynamiques==
Les processeurs précédents, avec 2 pipelines, avaient une unité d'émission assez simple. Elle avait deux ports de décodage, et deux ports d'émission. Le nombre de ports de décodage et d'émission étaient identiques. Vous vous dites que c'est naturel, et intuitif, que c'est normal pour un processeur superscalaire. Mais en réalité, la majorité des processeurs superscalaires a plus de ports d'émission que de décodage. Expliquer pourquoi va cependant nous demander de parler de comment la superscalarité exploite les unités de calcul.
Posons-nous cette question : de combien d'unités un processeur superscalaire a besoin ? Les unités en question ne sont nécessairement des unité de calcul, l'unité mémoire et l'unité de branchement comptent aussi. La réponse dépend du nombre de voies. Si le ''front-end'' charge et décode un paquet de N instructions, exécuter ces N instructions simultanément demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section.
La réponse dépend aussi des contraintes d’appariement. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission parfaite devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement, ce qui ferait passer de 6 unités à 12. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, le cout en circuit encore plus prohibitif !
En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée, alors que la double émission parait plus adaptée.
Et c'est là que la différence entre ports d'émission et de décodage devient intéressante. L'idée est d'avoir une unité d'émission avec deux ports de décodage, et 4 port d'émission (un par unité). Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux micro-opérations tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu.
Les tout premiers CPU superscalaires étaient nombreux à utiliser cette technique. Les exemples classiques sont l'HyperSPARC, de l'alpha 21064 et du PowerPC 603. C'étaient des CPU double émission, avec une ALU entière, une unité mémoire et une FPU. Nous parlerons de '''partitionnement INT-FLOAT-MEM'''. Les contraintes d’appariement variaient grandement suivant le processeur.
Le PowerPC 603 implémentait une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur.
Avant de poursuivre, précisons que la FPU regroupe un additionneur flottant et un multiplieur flottant. Et ce fait peut être exploité pour approfondir le partitionnement. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions).
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple, facile à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. La raison est que le partitionnement permet d'émettre des paires, triplets, quadruplets de micro-opérations qui sont rares en pratique.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''.
Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, 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. Et enfin, il faut rajouter un port d'émission pour pouvoir émettre une micro-opération entière en plus.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle.
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
Reste à connecter les unités de calcul à l'unité d'émission (indirectement, car il y a des étages de pipeline entre les deux). Une implémentation basique utilisee un port d'émission par unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La présence d'un circuit multiplieur et d'un ''barrel shifter'' pose des problèmes pour le partitionnement. Il est possible d'avoir un port d'émission dédié au multiplieur, et un autre au ''barrel shifter'', qui sont séparés de ceux pour l'ALU entière. Il est alors possible d'émettre une opération entière, une multiplication et un décalage, simultanément. Nous parlerons de '''partitionnement entier''' pour désigner cette possibilité.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
Le partitionnement entier est cependant très rare, car le gain en performance serait trop faible au regard des couts. Quelques processeurs historiques l'utilisaient, mais ce n'est plus le cas de nos jours. La raison est que les multiplications et les décalages sont assez rares dans le code, ce qui fait que le port d'émission associés sont sous-utilisés. Et un port d'émission a un cout en circuit assez important. 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.
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul. L'exemple classique est justement lié aux circuits multiplieur et ''barrel shifters''. L'idée est que le multiplieur et le ''barrel shifter'' sont mis sur le même port d'émission qu'une ALU entière.
Prenons par exemple le processeur à double émission entière illustré ci-contre. Un port d'émission est relié à une ALU entière seule, et l'autre port d'émission alimente la seconde ALU entière et le multiplieur. La conséquence est que le processeur peut émettre deux opérations entières, dont une multiplication. Si on avait utilisé un port d'émission dédié au multiplieur, il aurait été possible d'émettre deux opérations entières et une multiplication. On perd donc une voie, mais on gagne en flexibilité et on économise des circuits.
[[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]]
En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais le partage des ports d'émission peut appliquer à n'importe quelle unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. 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.
La solution est de partager des ports d'émission, pour passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple relie une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites. Le CPU peut émettre 6 opérations entières simultanément, grâce à 6 ALU entières. Les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires mélangent les trois techniques===
Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autre unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents.
* Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU.
* La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants.
* La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables.
* Le '''partage des ports d'émission''' réduit le cout en circuit du CPU, pour des pertes de performances mineures si bien fait.
De nos jours, le partitionnement est utilisé en complément de la duplication des ALU entières, de la duplication des autres unités, et du partage des ports d'émission. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. Le partage des ports d'émission a lui aussi mis un peu de temps pour s'imposer.
Les CPU superscalaires tendent à mettre le paquet niveau opérations arithmétiques, mais pas forcément au point de remplir la totalité de la largeur du processeur. La règle est que pour N voies, le processeur dispose d'au moins N/2 ALU entières. Attention, cela ne signifie pas que le processeur émet systématiquement N/2 opération entières. Il peut par exemple émettre une combinaison d'opérations flottantes et mémoire, avec des branchements. Et N/2 est une borne basse, la plupart des CPU x86 ont environ tournent plutôt autour des 2/3, voire des 3/4.
En général, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. La conséquence est que les unités mémoire sont dupliquées, en seconde place dans l'ordre d'importance. Les processeurs x86 ont commencé à dupliquer leurs unités mémoire dès qu'ils ont atteint la quadruple émission, pour les CPU grand public. Par exemple, la microarchitecture ''Golden Cove'' d'Intel dispose de cinq unités de calcul, trois unités mémoire (deux spécialisées dans les écritures, trois dans les lectures). La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire. Il y a donc soit égalité, soit avantage pour les ALU entières.
Prenons l'exemple des 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
|}
Les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon, sont un exemple moins représentatif. Ces microarchitectures utilisent le partage des ports d'émission au maximum. C'était des microarchitectures à triple émission, avec six ports d’émission. Il y avait trois prots d'émission pour les opérations flottantes, et trois ports d'émission pour les micro-opérations entières/mémoire. Il y avait trois ALU entières et trois unités mémoire, chaque ALU partageait un port d'émission avec une unité mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements.
{|class="wikitable"
|-
! Port 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 || ALU entière || FADD || FMUL || FPU (autres opérations)
|-
| Multiplieur || || || || ||
|-
| LOAD/STORE || LOAD/STORE || LOAD/STORE || || ||
|}
Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair.
Par exemple, la microarchitecture ''Golden Cove'' a deux ''barrel shifters''.
{|class="wikitable"
|-
! Port 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 !! Port d'émission n°7 !! Port d'émission n°8 !! Port d'émission n°9 !! Port d'émission n°10
|-
| ALU entière || ALU entière || ALU entière || ALU entière || ALU entière || LOAD || LOAD || LOAD || STORE || STORE
|-
| ''Barrel shifter'' || Multiplieur || Multiplieur || ''Barrel shifter'' || || || || || ||
|-
| Branchements || Diviseur || || Branchements || || || || || ||
|}
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les CPU superscalaire à exécution dans le désordre==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Les unités d'exécution dans le désordre===
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports de ''dispatch''. Mais avec des stations de réservation, il faut le double des ports de ''schedule''. La raison est que les registres sont lus après émission avec une fenê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 de ''schedule'' que de ''dispatch''.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de 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===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Avec plusieurs dizaines d'unités de calcul différentes, ajouter autant 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.
La première solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Le processeur détecte quand il n'y a pas assez de ports pour servir toutes les micro-opérations. Quand ça arrive, l'unité d'émission émet assez de micro-opérations pour exploiter les ports disponibles, mais met en attente les micro-opérations en trop, le temps que les ports se libèrent. Pour cela, le banc de registre est géré par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Une autre solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une dernière solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
==L'évolution historique de la superscalarité==
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. La plupart des CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 sont rapidement montés à de la quadruple émission, avec quelques exceptions. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 ||
|-
! 1995
| 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000
|-
! 1996
| class="f_bleu | 620 || || || || class="f_bleu | 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
===Les processeurs historiques à double émission===
Pour commencer, étudions le cas des premiers CPU superscalaires à double émission. Ils sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Notamment, la duplication des autres unités ne sert pour ainsi dire à rien, ce qui limite les possibilités, mais rendra les explications plus simples.
Il pouvaient se classer en trois types principaux, avec quelques processeurs qui ne rentraient pas dans ces cases. Le premier type utilise un ''partitionnement complet'' entre ALU, FPU et unité mémoire. Le second utilisait la double émission entière-flottante. Le troisième utilisait la double émission entière. Voici le tableau chronologique disant quel CPU utilisait quelle technique.
* Le partitionnement INT-FLOAT-MEM est en jaune.
* La double émission entière-flottante est en vert.
* La double émission entière est en rouge.
* Les CPU barrés utilisent la quadruple émission et seront vu dans la section suivante.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| class="f_vert | RS/6000 || || || class="f_rouge | i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| class="f_vert | RSC || class="f_vert | PA 7100 || SuperSPARC || || class="f_jaune | 21064 ||
|-
! 1993
| class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || ||
|-
! 1994
| <s>POWER 2</s> || PA 7200 || || || <s>21164</s> ||
|}
Les processeurs laissés de côté dans le tableau précédent ne rentrent pas dans les catégories précédentes.
Le PA 7200 est un PA-7100 auquel on aurait rajouté une ALU entière. Le processeur exécutait les multiplications entières dans la FPU, comme son prédécesseur. Les combinaisons autorisées étaient donc INT + INT, INT + MEM, INT + FLOAT/MUL, MEM + FLOAT/MUL.
Le SuperSPARC était un CPU triple émission, avec deux ALU entières, une FPU et une unité mémoire. Il avait de nombreuses contraintes d’appariement. Par exemple, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Une bonne partie de ces contraintes était lié au banc de registre, qui n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions. Il faut noter que la seconde ALU a été abandonnée lors du passage au CPU HyperSPARC, qui est de plus repassé en double émission, ce qui est une évolution à rebours de l'évolution des autres CPUs.
===Les processeurs historiques à quadruple émission===
C'est la même année , en 1994, que la quadruple émission est arrivée et que les CPU ont commencé à avoir de plus en plus d'unités de calcul. Même les designs restés en double émission avaient ajouté des unités de calcul. Il y avait alors trois stratégies pour la quadruple émission.
La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Les FPUs étaient soit dupliquées, soit scindées en deux en séparant l'additionneur du multiplieur flottant. Le POWER 2 dupliquait ses FPUs à la perfection, alors que l'21264 partitionnait la FPU. Les accès mémoire étaient traités comme des opérations entières, et le processeur pouvait : soit en faire un seul par cycle, soit en émettre deux, sous conditions (le cache devait être double port pour lire/écrire deux données à la fois, comme on le verra dans la suite du chapitre).
Les processeurs MIPS R8000 et R10000 allaient un peu plus loin, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités.
La seconde implémentation dupliquait l'ALU entière et combinait le tout avec un partitionnement INT-FLOAT-MEM. De quoi exécuter en même temps deux opérations entières, une opération flottante, et un accès mémoire. 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).
Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, avec au minimum deux ALU entières. Pour comprendre ce que je veux dire, prenons l'exemple de l'UltraSPARC. Il avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplier flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes. Le processeur pouvait émettre 4 instructions, tant qu'elles atterrissaient dans des unités différentes.
La même méthode peut être utilisée pour la double émission. Le Motorola 88110, sorti en 1991, était un CPU à double émission avec pas moins de 10 unités : deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise.
La morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes. Le tout est résumé dans le tableau ci-dessous, avec :
* en vert les CPU utilisant la première stratégie ;
* en rouge les CPU utilisant la seconde stratégie ;
* en jaune les CPU utilisant la troisième stratégie.
{|class="wikitable"
|+ Processeurs quadruple émission historiques
|-
! Année !! Processeur !! Unités
|-
! 1991
| class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques
|-
|-
! rowspan="2" | 1994
| class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'')
|-
| class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM
|-
! rowspan="3" | 1995
| class="f_jaune | PA 8000 || class="f_jaune | 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD
|-
| class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques
|-
| MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM
|-
! rowspan="2" | 1996
| class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM
|-
| class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL)
|}
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
6b48r6k231nhiysr9jce2al7b382brv
773192
773191
2026-09-25T20:45:44Z
Mewtow
31375
/* Les CPU superscalaires mélangent les trois techniques */
773192
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===L'impact des contraintes d’appariement sur la performance===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour la suite du chapitre===
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==Le ''front-end'' des processeurs superscalaires==
Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple.
Intuitivement, charger, décoder et 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 certaines processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. 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
|}
Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs.
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===L'émission parallèle des instructions===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elle sont propagée au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage).
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
==Les processeurs implémentés avec plusieurs pipelines==
Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Vous vous dites que l'implémentation d'un processeur superscalaire n'est pas la même suivant que le CPU utilise l'exécution dans le désordre, ou non. Mais dans les faits, la distinction est plus subtile que ca. Pour l'expliquer, je vais utiliser une classification originale, qui sépare les processeurs superscalaires en trois sous-types. La distinction se fait sur la manière dont la superscalarité est implémentée en matériel.La classification que j'utilise distingue :
* Les processeurs avec plusieurs pipelines, sans exécution dans le désordre.
* Les processeurs superscalaires avec un pipeline dynamique.
* Les processeurs avec des fenêtres d'instruction décentralisées.
La distinction n'est pas qu'une question d'implémentation matérielle. Elle impacte aussi le ratio entre instructions chargées/décodées et instructions émises. Vous vous dites que les deux sont égales, mais ce n'est le cas que sur les processeurs avec N pipelines ! Sur les autres processeurs, il est possible de décoder 4 instructions, mais d'en émettre maximum 6 par cycle ! Et sur les CPU avec des fenêtres d'instruction décentralisées, il y a aussi une différence entre le nombre d'instructions qui sont ''dispatch'' et ''schedule''.
La catégorie la plus simple est celle des '''processeurs avec plusieurs pipelines'''. Ce sont des processeurs superscalaires sans exécution dans le désordre. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction.
Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, d'une FPU, éventuellement d'un second banc de registres, guère plus. Il permet d'exécuter une seconde opération de calcul, pas plus, ce qui est à l'origine de contraintes d'appariemment assez fortes. De plus, il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section.
===La double émission entière-flottante===
L'exemple le plus simple utilise un pipeline pour les opérations flottantes, séparé du pipeline originel. Pour bien expliquer les choses, prenons le processeur illustré ci-dessous. Un processeur a deux ''back-end'' séparés, un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par l'ALU entière, passent éventuellement dans l'unité mémoire, pour ensuite arriver à l'étape de ''writeback''. La FPU et le banc de registre flottant ont eux aussi leur propre ''back-end'' séparé.
[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]]
Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaqaue pipeline, à chaque cycle.
L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine.
Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants.
===La double émission entière===
Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait.
Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
==Les processeurs superscalaires dynamiques==
Les processeurs précédents, avec 2 pipelines, avaient une unité d'émission assez simple. Elle avait deux ports de décodage, et deux ports d'émission. Le nombre de ports de décodage et d'émission étaient identiques. Vous vous dites que c'est naturel, et intuitif, que c'est normal pour un processeur superscalaire. Mais en réalité, la majorité des processeurs superscalaires a plus de ports d'émission que de décodage. Expliquer pourquoi va cependant nous demander de parler de comment la superscalarité exploite les unités de calcul.
Posons-nous cette question : de combien d'unités un processeur superscalaire a besoin ? Les unités en question ne sont nécessairement des unité de calcul, l'unité mémoire et l'unité de branchement comptent aussi. La réponse dépend du nombre de voies. Si le ''front-end'' charge et décode un paquet de N instructions, exécuter ces N instructions simultanément demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section.
La réponse dépend aussi des contraintes d’appariement. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission parfaite devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement, ce qui ferait passer de 6 unités à 12. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, le cout en circuit encore plus prohibitif !
En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée, alors que la double émission parait plus adaptée.
Et c'est là que la différence entre ports d'émission et de décodage devient intéressante. L'idée est d'avoir une unité d'émission avec deux ports de décodage, et 4 port d'émission (un par unité). Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux micro-opérations tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu.
Les tout premiers CPU superscalaires étaient nombreux à utiliser cette technique. Les exemples classiques sont l'HyperSPARC, de l'alpha 21064 et du PowerPC 603. C'étaient des CPU double émission, avec une ALU entière, une unité mémoire et une FPU. Nous parlerons de '''partitionnement INT-FLOAT-MEM'''. Les contraintes d’appariement variaient grandement suivant le processeur.
Le PowerPC 603 implémentait une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur.
Avant de poursuivre, précisons que la FPU regroupe un additionneur flottant et un multiplieur flottant. Et ce fait peut être exploité pour approfondir le partitionnement. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions).
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple, facile à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. La raison est que le partitionnement permet d'émettre des paires, triplets, quadruplets de micro-opérations qui sont rares en pratique.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''.
Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, 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. Et enfin, il faut rajouter un port d'émission pour pouvoir émettre une micro-opération entière en plus.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle.
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
Reste à connecter les unités de calcul à l'unité d'émission (indirectement, car il y a des étages de pipeline entre les deux). Une implémentation basique utilisee un port d'émission par unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La présence d'un circuit multiplieur et d'un ''barrel shifter'' pose des problèmes pour le partitionnement. Il est possible d'avoir un port d'émission dédié au multiplieur, et un autre au ''barrel shifter'', qui sont séparés de ceux pour l'ALU entière. Il est alors possible d'émettre une opération entière, une multiplication et un décalage, simultanément. Nous parlerons de '''partitionnement entier''' pour désigner cette possibilité.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
Le partitionnement entier est cependant très rare, car le gain en performance serait trop faible au regard des couts. Quelques processeurs historiques l'utilisaient, mais ce n'est plus le cas de nos jours. La raison est que les multiplications et les décalages sont assez rares dans le code, ce qui fait que le port d'émission associés sont sous-utilisés. Et un port d'émission a un cout en circuit assez important. 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.
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul. L'exemple classique est justement lié aux circuits multiplieur et ''barrel shifters''. L'idée est que le multiplieur et le ''barrel shifter'' sont mis sur le même port d'émission qu'une ALU entière.
Prenons par exemple le processeur à double émission entière illustré ci-contre. Un port d'émission est relié à une ALU entière seule, et l'autre port d'émission alimente la seconde ALU entière et le multiplieur. La conséquence est que le processeur peut émettre deux opérations entières, dont une multiplication. Si on avait utilisé un port d'émission dédié au multiplieur, il aurait été possible d'émettre deux opérations entières et une multiplication. On perd donc une voie, mais on gagne en flexibilité et on économise des circuits.
[[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]]
En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais le partage des ports d'émission peut appliquer à n'importe quelle unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. 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.
La solution est de partager des ports d'émission, pour passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple relie une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites. Le CPU peut émettre 6 opérations entières simultanément, grâce à 6 ALU entières. Les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires mélangent les trois techniques===
Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autre unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents.
* Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU.
* La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants.
* La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables.
* Le '''partage des ports d'émission''' réduit le cout en circuit du CPU, pour des pertes de performances mineures si bien fait.
De nos jours, le partitionnement est utilisé en complément de la duplication des ALU entières, de la duplication des autres unités, et du partage des ports d'émission. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. Le partage des ports d'émission a lui aussi mis un peu de temps pour s'imposer.
Les CPU superscalaires tendent à mettre le paquet niveau opérations arithmétiques, mais pas forcément au point de remplir la totalité de la largeur du processeur. La règle est que pour N voies, le processeur dispose d'au moins N/2 ALU entières. Attention, cela ne signifie pas que le processeur émet systématiquement N/2 opération entières. Il peut par exemple émettre une combinaison d'opérations flottantes et mémoire, avec des branchements. Et N/2 est une borne basse, la plupart des CPU x86 ont environ tournent plutôt autour des 2/3, voire des 3/4.
En général, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. La conséquence est que les unités mémoire sont dupliquées, en seconde place dans l'ordre d'importance. Les processeurs x86 ont commencé à dupliquer leurs unités mémoire dès qu'ils ont atteint la quadruple émission, pour les CPU grand public. Par exemple, la microarchitecture ''Golden Cove'' d'Intel dispose de cinq unités de calcul, trois unités mémoire (deux spécialisées dans les écritures, trois dans les lectures). La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire. Il y a donc soit égalité, soit avantage pour les ALU entières.
Prenons l'exemple des 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. C'était des processeurs quadruple émission, ce qui fait que la moitié de la largeur du processeur sert pour des opérations entières, et l'autre moitié pour les accès mémoire.
{|class="wikitable"
|-
! Port 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
|}
Les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon, sont un exemple moins représentatif. Ces microarchitectures utilisent le partage des ports d'émission au maximum. C'était des microarchitectures à triple émission, avec six ports d’émission. Il y avait trois ports d'émission pour les opérations flottantes, et trois ports d'émission pour les micro-opérations entières/mémoire. Il y avait trois ALU entières et trois unités mémoire, chaque ALU partageait un port d'émission avec une unité mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements.
{|class="wikitable"
|-
! Port 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 || ALU entière || FADD || FMUL || FPU (autres opérations)
|-
| Multiplieur || || || || ||
|-
| LOAD/STORE || LOAD/STORE || LOAD/STORE || || ||
|}
Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair.
Par exemple, la microarchitecture ''Golden Cove'' a deux ''barrel shifters''.
{|class="wikitable"
|-
! Port 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 !! Port d'émission n°7 !! Port d'émission n°8 !! Port d'émission n°9 !! Port d'émission n°10
|-
| ALU entière || ALU entière || ALU entière || ALU entière || ALU entière || LOAD || LOAD || LOAD || STORE || STORE
|-
| ''Barrel shifter'' || Multiplieur || Multiplieur || ''Barrel shifter'' || || || || || ||
|-
| Branchements || Diviseur || || Branchements || || || || || ||
|}
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les CPU superscalaire à exécution dans le désordre==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Les unités d'exécution dans le désordre===
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports de ''dispatch''. Mais avec des stations de réservation, il faut le double des ports de ''schedule''. La raison est que les registres sont lus après émission avec une fenê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 de ''schedule'' que de ''dispatch''.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de 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===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Avec plusieurs dizaines d'unités de calcul différentes, ajouter autant 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.
La première solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Le processeur détecte quand il n'y a pas assez de ports pour servir toutes les micro-opérations. Quand ça arrive, l'unité d'émission émet assez de micro-opérations pour exploiter les ports disponibles, mais met en attente les micro-opérations en trop, le temps que les ports se libèrent. Pour cela, le banc de registre est géré par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Une autre solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une dernière solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
==L'évolution historique de la superscalarité==
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. La plupart des CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 sont rapidement montés à de la quadruple émission, avec quelques exceptions. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 ||
|-
! 1995
| 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000
|-
! 1996
| class="f_bleu | 620 || || || || class="f_bleu | 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
===Les processeurs historiques à double émission===
Pour commencer, étudions le cas des premiers CPU superscalaires à double émission. Ils sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Notamment, la duplication des autres unités ne sert pour ainsi dire à rien, ce qui limite les possibilités, mais rendra les explications plus simples.
Il pouvaient se classer en trois types principaux, avec quelques processeurs qui ne rentraient pas dans ces cases. Le premier type utilise un ''partitionnement complet'' entre ALU, FPU et unité mémoire. Le second utilisait la double émission entière-flottante. Le troisième utilisait la double émission entière. Voici le tableau chronologique disant quel CPU utilisait quelle technique.
* Le partitionnement INT-FLOAT-MEM est en jaune.
* La double émission entière-flottante est en vert.
* La double émission entière est en rouge.
* Les CPU barrés utilisent la quadruple émission et seront vu dans la section suivante.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| class="f_vert | RS/6000 || || || class="f_rouge | i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| class="f_vert | RSC || class="f_vert | PA 7100 || SuperSPARC || || class="f_jaune | 21064 ||
|-
! 1993
| class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || ||
|-
! 1994
| <s>POWER 2</s> || PA 7200 || || || <s>21164</s> ||
|}
Les processeurs laissés de côté dans le tableau précédent ne rentrent pas dans les catégories précédentes.
Le PA 7200 est un PA-7100 auquel on aurait rajouté une ALU entière. Le processeur exécutait les multiplications entières dans la FPU, comme son prédécesseur. Les combinaisons autorisées étaient donc INT + INT, INT + MEM, INT + FLOAT/MUL, MEM + FLOAT/MUL.
Le SuperSPARC était un CPU triple émission, avec deux ALU entières, une FPU et une unité mémoire. Il avait de nombreuses contraintes d’appariement. Par exemple, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Une bonne partie de ces contraintes était lié au banc de registre, qui n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions. Il faut noter que la seconde ALU a été abandonnée lors du passage au CPU HyperSPARC, qui est de plus repassé en double émission, ce qui est une évolution à rebours de l'évolution des autres CPUs.
===Les processeurs historiques à quadruple émission===
C'est la même année , en 1994, que la quadruple émission est arrivée et que les CPU ont commencé à avoir de plus en plus d'unités de calcul. Même les designs restés en double émission avaient ajouté des unités de calcul. Il y avait alors trois stratégies pour la quadruple émission.
La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Les FPUs étaient soit dupliquées, soit scindées en deux en séparant l'additionneur du multiplieur flottant. Le POWER 2 dupliquait ses FPUs à la perfection, alors que l'21264 partitionnait la FPU. Les accès mémoire étaient traités comme des opérations entières, et le processeur pouvait : soit en faire un seul par cycle, soit en émettre deux, sous conditions (le cache devait être double port pour lire/écrire deux données à la fois, comme on le verra dans la suite du chapitre).
Les processeurs MIPS R8000 et R10000 allaient un peu plus loin, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités.
La seconde implémentation dupliquait l'ALU entière et combinait le tout avec un partitionnement INT-FLOAT-MEM. De quoi exécuter en même temps deux opérations entières, une opération flottante, et un accès mémoire. 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).
Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, avec au minimum deux ALU entières. Pour comprendre ce que je veux dire, prenons l'exemple de l'UltraSPARC. Il avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplier flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes. Le processeur pouvait émettre 4 instructions, tant qu'elles atterrissaient dans des unités différentes.
La même méthode peut être utilisée pour la double émission. Le Motorola 88110, sorti en 1991, était un CPU à double émission avec pas moins de 10 unités : deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise.
La morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes. Le tout est résumé dans le tableau ci-dessous, avec :
* en vert les CPU utilisant la première stratégie ;
* en rouge les CPU utilisant la seconde stratégie ;
* en jaune les CPU utilisant la troisième stratégie.
{|class="wikitable"
|+ Processeurs quadruple émission historiques
|-
! Année !! Processeur !! Unités
|-
! 1991
| class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques
|-
|-
! rowspan="2" | 1994
| class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'')
|-
| class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM
|-
! rowspan="3" | 1995
| class="f_jaune | PA 8000 || class="f_jaune | 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD
|-
| class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques
|-
| MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM
|-
! rowspan="2" | 1996
| class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM
|-
| class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL)
|}
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
pmgl6gxim3valn26lkhy7ph4rbwor3z
773193
773192
2026-09-25T20:49:13Z
Mewtow
31375
/* Les CPU superscalaires mélangent les trois techniques */
773193
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===L'impact des contraintes d’appariement sur la performance===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour la suite du chapitre===
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==Le ''front-end'' des processeurs superscalaires==
Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple.
Intuitivement, charger, décoder et 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 certaines processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. 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
|}
Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs.
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===L'émission parallèle des instructions===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elle sont propagée au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage).
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
==Les processeurs implémentés avec plusieurs pipelines==
Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Vous vous dites que l'implémentation d'un processeur superscalaire n'est pas la même suivant que le CPU utilise l'exécution dans le désordre, ou non. Mais dans les faits, la distinction est plus subtile que ca. Pour l'expliquer, je vais utiliser une classification originale, qui sépare les processeurs superscalaires en trois sous-types. La distinction se fait sur la manière dont la superscalarité est implémentée en matériel.La classification que j'utilise distingue :
* Les processeurs avec plusieurs pipelines, sans exécution dans le désordre.
* Les processeurs superscalaires avec un pipeline dynamique.
* Les processeurs avec des fenêtres d'instruction décentralisées.
La distinction n'est pas qu'une question d'implémentation matérielle. Elle impacte aussi le ratio entre instructions chargées/décodées et instructions émises. Vous vous dites que les deux sont égales, mais ce n'est le cas que sur les processeurs avec N pipelines ! Sur les autres processeurs, il est possible de décoder 4 instructions, mais d'en émettre maximum 6 par cycle ! Et sur les CPU avec des fenêtres d'instruction décentralisées, il y a aussi une différence entre le nombre d'instructions qui sont ''dispatch'' et ''schedule''.
La catégorie la plus simple est celle des '''processeurs avec plusieurs pipelines'''. Ce sont des processeurs superscalaires sans exécution dans le désordre. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction.
Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, d'une FPU, éventuellement d'un second banc de registres, guère plus. Il permet d'exécuter une seconde opération de calcul, pas plus, ce qui est à l'origine de contraintes d'appariemment assez fortes. De plus, il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section.
===La double émission entière-flottante===
L'exemple le plus simple utilise un pipeline pour les opérations flottantes, séparé du pipeline originel. Pour bien expliquer les choses, prenons le processeur illustré ci-dessous. Un processeur a deux ''back-end'' séparés, un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par l'ALU entière, passent éventuellement dans l'unité mémoire, pour ensuite arriver à l'étape de ''writeback''. La FPU et le banc de registre flottant ont eux aussi leur propre ''back-end'' séparé.
[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]]
Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaqaue pipeline, à chaque cycle.
L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine.
Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants.
===La double émission entière===
Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait.
Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
==Les processeurs superscalaires dynamiques==
Les processeurs précédents, avec 2 pipelines, avaient une unité d'émission assez simple. Elle avait deux ports de décodage, et deux ports d'émission. Le nombre de ports de décodage et d'émission étaient identiques. Vous vous dites que c'est naturel, et intuitif, que c'est normal pour un processeur superscalaire. Mais en réalité, la majorité des processeurs superscalaires a plus de ports d'émission que de décodage. Expliquer pourquoi va cependant nous demander de parler de comment la superscalarité exploite les unités de calcul.
Posons-nous cette question : de combien d'unités un processeur superscalaire a besoin ? Les unités en question ne sont nécessairement des unité de calcul, l'unité mémoire et l'unité de branchement comptent aussi. La réponse dépend du nombre de voies. Si le ''front-end'' charge et décode un paquet de N instructions, exécuter ces N instructions simultanément demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section.
La réponse dépend aussi des contraintes d’appariement. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission parfaite devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement, ce qui ferait passer de 6 unités à 12. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, le cout en circuit encore plus prohibitif !
En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée, alors que la double émission parait plus adaptée.
Et c'est là que la différence entre ports d'émission et de décodage devient intéressante. L'idée est d'avoir une unité d'émission avec deux ports de décodage, et 4 port d'émission (un par unité). Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux micro-opérations tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu.
Les tout premiers CPU superscalaires étaient nombreux à utiliser cette technique. Les exemples classiques sont l'HyperSPARC, de l'alpha 21064 et du PowerPC 603. C'étaient des CPU double émission, avec une ALU entière, une unité mémoire et une FPU. Nous parlerons de '''partitionnement INT-FLOAT-MEM'''. Les contraintes d’appariement variaient grandement suivant le processeur.
Le PowerPC 603 implémentait une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur.
Avant de poursuivre, précisons que la FPU regroupe un additionneur flottant et un multiplieur flottant. Et ce fait peut être exploité pour approfondir le partitionnement. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions).
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple, facile à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. La raison est que le partitionnement permet d'émettre des paires, triplets, quadruplets de micro-opérations qui sont rares en pratique.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''.
Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, 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. Et enfin, il faut rajouter un port d'émission pour pouvoir émettre une micro-opération entière en plus.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle.
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
Reste à connecter les unités de calcul à l'unité d'émission (indirectement, car il y a des étages de pipeline entre les deux). Une implémentation basique utilisee un port d'émission par unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La présence d'un circuit multiplieur et d'un ''barrel shifter'' pose des problèmes pour le partitionnement. Il est possible d'avoir un port d'émission dédié au multiplieur, et un autre au ''barrel shifter'', qui sont séparés de ceux pour l'ALU entière. Il est alors possible d'émettre une opération entière, une multiplication et un décalage, simultanément. Nous parlerons de '''partitionnement entier''' pour désigner cette possibilité.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
Le partitionnement entier est cependant très rare, car le gain en performance serait trop faible au regard des couts. Quelques processeurs historiques l'utilisaient, mais ce n'est plus le cas de nos jours. La raison est que les multiplications et les décalages sont assez rares dans le code, ce qui fait que le port d'émission associés sont sous-utilisés. Et un port d'émission a un cout en circuit assez important. 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.
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul. L'exemple classique est justement lié aux circuits multiplieur et ''barrel shifters''. L'idée est que le multiplieur et le ''barrel shifter'' sont mis sur le même port d'émission qu'une ALU entière.
Prenons par exemple le processeur à double émission entière illustré ci-contre. Un port d'émission est relié à une ALU entière seule, et l'autre port d'émission alimente la seconde ALU entière et le multiplieur. La conséquence est que le processeur peut émettre deux opérations entières, dont une multiplication. Si on avait utilisé un port d'émission dédié au multiplieur, il aurait été possible d'émettre deux opérations entières et une multiplication. On perd donc une voie, mais on gagne en flexibilité et on économise des circuits.
[[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]]
En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais le partage des ports d'émission peut appliquer à n'importe quelle unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. 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.
La solution est de partager des ports d'émission, pour passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple relie une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites. Le CPU peut émettre 6 opérations entières simultanément, grâce à 6 ALU entières. Les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires mélangent les trois techniques===
Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autre unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents.
* Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU.
* La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants.
* La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables.
* Le '''partage des ports d'émission''' réduit le cout en circuit du CPU, pour des pertes de performances mineures si bien fait.
De nos jours, le partitionnement est utilisé en complément de la duplication des ALU entières, de la duplication des autres unités, et du partage des ports d'émission. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. Le partage des ports d'émission a lui aussi mis un peu de temps pour s'imposer.
Les CPU superscalaires tendent à mettre le paquet niveau opérations arithmétiques, mais pas forcément au point de remplir la totalité de la largeur du processeur. La règle est que pour N voies, le processeur dispose d'au moins N/2 ALU entières. Attention, cela ne signifie pas que le processeur émet systématiquement N/2 opération entières. Il peut par exemple émettre une combinaison d'opérations flottantes et mémoire, avec des branchements. Et N/2 est une borne basse, la plupart des CPU x86 ont environ tournent plutôt autour des 2/3, voire des 3/4.
En général, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. La conséquence est que les unités mémoire sont dupliquées, en seconde place dans l'ordre d'importance. Les processeurs x86 ont commencé à dupliquer leurs unités mémoire dès qu'ils ont atteint la quadruple émission, pour les CPU grand public. Par exemple, la microarchitecture ''Golden Cove'' d'Intel dispose de cinq unités de calcul, trois unités mémoire (deux spécialisées dans les écritures, trois dans les lectures). La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire. Il y a donc soit égalité, soit avantage pour les ALU entières.
Une autre régularité est que la largeur totale du processeur peut être occupée par des opérations entières/mémoire. Pour le dire autrement, sur un processeur quadruple émission, il pourra émettre un mix de 4 instructions entière/mémoire, avec zéro opération flottante. Idem avec des processeurs triples ou pentuple émission, voire plus. Si le processeur émet une opération flottante, elle remplace une opération entière.
Prenons l'exemple des 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. C'était des processeurs quadruple émission, ce qui fait que la moitié de la largeur du processeur sert pour des opérations entières, et l'autre moitié pour les accès mémoire.
{|class="wikitable"
|-
! Port 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
|}
Les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon, sont un exemple moins représentatif. Ces microarchitectures utilisent le partage des ports d'émission au maximum. C'était des microarchitectures à triple émission, avec six ports d’émission. Il y avait trois ports d'émission pour les opérations flottantes, et trois ports d'émission pour les micro-opérations entières/mémoire. Il y avait trois ALU entières et trois unités mémoire, chaque ALU partageait un port d'émission avec une unité mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements.
{|class="wikitable"
|-
! Port 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 || ALU entière || FADD || FMUL || FPU (autres opérations)
|-
| Multiplieur || || || || ||
|-
| LOAD/STORE || LOAD/STORE || LOAD/STORE || || ||
|}
Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair.
Par exemple, la microarchitecture ''Golden Cove'' a deux ''barrel shifters''.
{|class="wikitable"
|-
! Port 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 !! Port d'émission n°7 !! Port d'émission n°8 !! Port d'émission n°9 !! Port d'émission n°10
|-
| ALU entière || ALU entière || ALU entière || ALU entière || ALU entière || LOAD || LOAD || LOAD || STORE || STORE
|-
| ''Barrel shifter'' || Multiplieur || Multiplieur || ''Barrel shifter'' || || || || || ||
|-
| Branchements || Diviseur || || Branchements || || || || || ||
|}
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les CPU superscalaire à exécution dans le désordre==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Les unités d'exécution dans le désordre===
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports de ''dispatch''. Mais avec des stations de réservation, il faut le double des ports de ''schedule''. La raison est que les registres sont lus après émission avec une fenê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 de ''schedule'' que de ''dispatch''.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de 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===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Avec plusieurs dizaines d'unités de calcul différentes, ajouter autant 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.
La première solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Le processeur détecte quand il n'y a pas assez de ports pour servir toutes les micro-opérations. Quand ça arrive, l'unité d'émission émet assez de micro-opérations pour exploiter les ports disponibles, mais met en attente les micro-opérations en trop, le temps que les ports se libèrent. Pour cela, le banc de registre est géré par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Une autre solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une dernière solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
==L'évolution historique de la superscalarité==
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. La plupart des CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 sont rapidement montés à de la quadruple émission, avec quelques exceptions. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 ||
|-
! 1995
| 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000
|-
! 1996
| class="f_bleu | 620 || || || || class="f_bleu | 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
===Les processeurs historiques à double émission===
Pour commencer, étudions le cas des premiers CPU superscalaires à double émission. Ils sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Notamment, la duplication des autres unités ne sert pour ainsi dire à rien, ce qui limite les possibilités, mais rendra les explications plus simples.
Il pouvaient se classer en trois types principaux, avec quelques processeurs qui ne rentraient pas dans ces cases. Le premier type utilise un ''partitionnement complet'' entre ALU, FPU et unité mémoire. Le second utilisait la double émission entière-flottante. Le troisième utilisait la double émission entière. Voici le tableau chronologique disant quel CPU utilisait quelle technique.
* Le partitionnement INT-FLOAT-MEM est en jaune.
* La double émission entière-flottante est en vert.
* La double émission entière est en rouge.
* Les CPU barrés utilisent la quadruple émission et seront vu dans la section suivante.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| class="f_vert | RS/6000 || || || class="f_rouge | i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| class="f_vert | RSC || class="f_vert | PA 7100 || SuperSPARC || || class="f_jaune | 21064 ||
|-
! 1993
| class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || ||
|-
! 1994
| <s>POWER 2</s> || PA 7200 || || || <s>21164</s> ||
|}
Les processeurs laissés de côté dans le tableau précédent ne rentrent pas dans les catégories précédentes.
Le PA 7200 est un PA-7100 auquel on aurait rajouté une ALU entière. Le processeur exécutait les multiplications entières dans la FPU, comme son prédécesseur. Les combinaisons autorisées étaient donc INT + INT, INT + MEM, INT + FLOAT/MUL, MEM + FLOAT/MUL.
Le SuperSPARC était un CPU triple émission, avec deux ALU entières, une FPU et une unité mémoire. Il avait de nombreuses contraintes d’appariement. Par exemple, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Une bonne partie de ces contraintes était lié au banc de registre, qui n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions. Il faut noter que la seconde ALU a été abandonnée lors du passage au CPU HyperSPARC, qui est de plus repassé en double émission, ce qui est une évolution à rebours de l'évolution des autres CPUs.
===Les processeurs historiques à quadruple émission===
C'est la même année , en 1994, que la quadruple émission est arrivée et que les CPU ont commencé à avoir de plus en plus d'unités de calcul. Même les designs restés en double émission avaient ajouté des unités de calcul. Il y avait alors trois stratégies pour la quadruple émission.
La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Les FPUs étaient soit dupliquées, soit scindées en deux en séparant l'additionneur du multiplieur flottant. Le POWER 2 dupliquait ses FPUs à la perfection, alors que l'21264 partitionnait la FPU. Les accès mémoire étaient traités comme des opérations entières, et le processeur pouvait : soit en faire un seul par cycle, soit en émettre deux, sous conditions (le cache devait être double port pour lire/écrire deux données à la fois, comme on le verra dans la suite du chapitre).
Les processeurs MIPS R8000 et R10000 allaient un peu plus loin, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités.
La seconde implémentation dupliquait l'ALU entière et combinait le tout avec un partitionnement INT-FLOAT-MEM. De quoi exécuter en même temps deux opérations entières, une opération flottante, et un accès mémoire. 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).
Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, avec au minimum deux ALU entières. Pour comprendre ce que je veux dire, prenons l'exemple de l'UltraSPARC. Il avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplier flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes. Le processeur pouvait émettre 4 instructions, tant qu'elles atterrissaient dans des unités différentes.
La même méthode peut être utilisée pour la double émission. Le Motorola 88110, sorti en 1991, était un CPU à double émission avec pas moins de 10 unités : deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise.
La morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes. Le tout est résumé dans le tableau ci-dessous, avec :
* en vert les CPU utilisant la première stratégie ;
* en rouge les CPU utilisant la seconde stratégie ;
* en jaune les CPU utilisant la troisième stratégie.
{|class="wikitable"
|+ Processeurs quadruple émission historiques
|-
! Année !! Processeur !! Unités
|-
! 1991
| class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques
|-
|-
! rowspan="2" | 1994
| class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'')
|-
| class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM
|-
! rowspan="3" | 1995
| class="f_jaune | PA 8000 || class="f_jaune | 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD
|-
| class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques
|-
| MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM
|-
! rowspan="2" | 1996
| class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM
|-
| class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL)
|}
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
qe54bzapdlm257sxfi4ggfgaasyr8o7
773194
773193
2026-09-25T20:51:43Z
Mewtow
31375
/* Les CPU superscalaires mélangent les trois techniques */
773194
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===L'impact des contraintes d’appariement sur la performance===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour la suite du chapitre===
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==Le ''front-end'' des processeurs superscalaires==
Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple.
Intuitivement, charger, décoder et 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 certaines processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. 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
|}
Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs.
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===L'émission parallèle des instructions===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elle sont propagée au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage).
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
==Les processeurs implémentés avec plusieurs pipelines==
Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Vous vous dites que l'implémentation d'un processeur superscalaire n'est pas la même suivant que le CPU utilise l'exécution dans le désordre, ou non. Mais dans les faits, la distinction est plus subtile que ca. Pour l'expliquer, je vais utiliser une classification originale, qui sépare les processeurs superscalaires en trois sous-types. La distinction se fait sur la manière dont la superscalarité est implémentée en matériel.La classification que j'utilise distingue :
* Les processeurs avec plusieurs pipelines, sans exécution dans le désordre.
* Les processeurs superscalaires avec un pipeline dynamique.
* Les processeurs avec des fenêtres d'instruction décentralisées.
La distinction n'est pas qu'une question d'implémentation matérielle. Elle impacte aussi le ratio entre instructions chargées/décodées et instructions émises. Vous vous dites que les deux sont égales, mais ce n'est le cas que sur les processeurs avec N pipelines ! Sur les autres processeurs, il est possible de décoder 4 instructions, mais d'en émettre maximum 6 par cycle ! Et sur les CPU avec des fenêtres d'instruction décentralisées, il y a aussi une différence entre le nombre d'instructions qui sont ''dispatch'' et ''schedule''.
La catégorie la plus simple est celle des '''processeurs avec plusieurs pipelines'''. Ce sont des processeurs superscalaires sans exécution dans le désordre. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction.
Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, d'une FPU, éventuellement d'un second banc de registres, guère plus. Il permet d'exécuter une seconde opération de calcul, pas plus, ce qui est à l'origine de contraintes d'appariemment assez fortes. De plus, il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section.
===La double émission entière-flottante===
L'exemple le plus simple utilise un pipeline pour les opérations flottantes, séparé du pipeline originel. Pour bien expliquer les choses, prenons le processeur illustré ci-dessous. Un processeur a deux ''back-end'' séparés, un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par l'ALU entière, passent éventuellement dans l'unité mémoire, pour ensuite arriver à l'étape de ''writeback''. La FPU et le banc de registre flottant ont eux aussi leur propre ''back-end'' séparé.
[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]]
Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaqaue pipeline, à chaque cycle.
L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine.
Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants.
===La double émission entière===
Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait.
Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
==Les processeurs superscalaires dynamiques==
Les processeurs précédents, avec 2 pipelines, avaient une unité d'émission assez simple. Elle avait deux ports de décodage, et deux ports d'émission. Le nombre de ports de décodage et d'émission étaient identiques. Vous vous dites que c'est naturel, et intuitif, que c'est normal pour un processeur superscalaire. Mais en réalité, la majorité des processeurs superscalaires a plus de ports d'émission que de décodage. Expliquer pourquoi va cependant nous demander de parler de comment la superscalarité exploite les unités de calcul.
Posons-nous cette question : de combien d'unités un processeur superscalaire a besoin ? Les unités en question ne sont nécessairement des unité de calcul, l'unité mémoire et l'unité de branchement comptent aussi. La réponse dépend du nombre de voies. Si le ''front-end'' charge et décode un paquet de N instructions, exécuter ces N instructions simultanément demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section.
La réponse dépend aussi des contraintes d’appariement. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission parfaite devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement, ce qui ferait passer de 6 unités à 12. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, le cout en circuit encore plus prohibitif !
En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée, alors que la double émission parait plus adaptée.
Et c'est là que la différence entre ports d'émission et de décodage devient intéressante. L'idée est d'avoir une unité d'émission avec deux ports de décodage, et 4 port d'émission (un par unité). Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux micro-opérations tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu.
Les tout premiers CPU superscalaires étaient nombreux à utiliser cette technique. Les exemples classiques sont l'HyperSPARC, de l'alpha 21064 et du PowerPC 603. C'étaient des CPU double émission, avec une ALU entière, une unité mémoire et une FPU. Nous parlerons de '''partitionnement INT-FLOAT-MEM'''. Les contraintes d’appariement variaient grandement suivant le processeur.
Le PowerPC 603 implémentait une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur.
Avant de poursuivre, précisons que la FPU regroupe un additionneur flottant et un multiplieur flottant. Et ce fait peut être exploité pour approfondir le partitionnement. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions).
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple, facile à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. La raison est que le partitionnement permet d'émettre des paires, triplets, quadruplets de micro-opérations qui sont rares en pratique.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''.
Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, 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. Et enfin, il faut rajouter un port d'émission pour pouvoir émettre une micro-opération entière en plus.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle.
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
Reste à connecter les unités de calcul à l'unité d'émission (indirectement, car il y a des étages de pipeline entre les deux). Une implémentation basique utilisee un port d'émission par unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La présence d'un circuit multiplieur et d'un ''barrel shifter'' pose des problèmes pour le partitionnement. Il est possible d'avoir un port d'émission dédié au multiplieur, et un autre au ''barrel shifter'', qui sont séparés de ceux pour l'ALU entière. Il est alors possible d'émettre une opération entière, une multiplication et un décalage, simultanément. Nous parlerons de '''partitionnement entier''' pour désigner cette possibilité.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
Le partitionnement entier est cependant très rare, car le gain en performance serait trop faible au regard des couts. Quelques processeurs historiques l'utilisaient, mais ce n'est plus le cas de nos jours. La raison est que les multiplications et les décalages sont assez rares dans le code, ce qui fait que le port d'émission associés sont sous-utilisés. Et un port d'émission a un cout en circuit assez important. 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.
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul. L'exemple classique est justement lié aux circuits multiplieur et ''barrel shifters''. L'idée est que le multiplieur et le ''barrel shifter'' sont mis sur le même port d'émission qu'une ALU entière.
Prenons par exemple le processeur à double émission entière illustré ci-contre. Un port d'émission est relié à une ALU entière seule, et l'autre port d'émission alimente la seconde ALU entière et le multiplieur. La conséquence est que le processeur peut émettre deux opérations entières, dont une multiplication. Si on avait utilisé un port d'émission dédié au multiplieur, il aurait été possible d'émettre deux opérations entières et une multiplication. On perd donc une voie, mais on gagne en flexibilité et on économise des circuits.
[[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]]
En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais le partage des ports d'émission peut appliquer à n'importe quelle unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. 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.
La solution est de partager des ports d'émission, pour passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple relie une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites. Le CPU peut émettre 6 opérations entières simultanément, grâce à 6 ALU entières. Les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires mélangent les trois techniques===
Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autre unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents.
* Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU.
* La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants.
* La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables.
* Le '''partage des ports d'émission''' réduit le cout en circuit du CPU, pour des pertes de performances mineures si bien fait.
De nos jours, le partitionnement est utilisé en complément de la duplication des ALU entières, de la duplication des autres unités, et du partage des ports d'émission. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. Le partage des ports d'émission a lui aussi mis un peu de temps pour s'imposer.
Les CPU superscalaires tendent à mettre le paquet niveau opérations arithmétiques, mais pas forcément au point de remplir la totalité de la largeur du processeur. La règle est que pour N voies, le processeur dispose d'au moins N/2 ALU entières. Et N/2 est une borne basse, la plupart des CPU x86 ont environ tournent plutôt autour des 2/3, voire des 3/4. Attention, cela ne signifie pas que le processeur émet systématiquement N/2 opération entières, il peut par exemple remplacer des opérations entières par des opérations flottantes.
En général, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. La conséquence est que les processeurs ont commencé à dupliquer leurs unités mémoire dès qu'ils ont atteint la quadruple émission, pour les CPU grand public. Par exemple, la microarchitecture ''Golden Cove'' d'Intel dispose de cinq unités de calcul, trois unités mémoire (deux spécialisées dans les écritures, trois dans les lectures). La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire. Notez que le ratio est équilibré entre opérations entières et mémoire, avec soit égalité, soit un avantage pour les ALU entières.
Une autre régularité est que la largeur totale du processeur peut être occupée par des opérations entières/mémoire. Pour le dire autrement, sur un processeur quadruple émission, il pourra émettre un mix de 4 instructions entière/mémoire, avec zéro opération flottante. Idem avec des processeurs triples ou pentuple émission, voire plus. Si le processeur émet une opération flottante, elle remplace une opération entière.
Prenons l'exemple des 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. C'était des processeurs quadruple émission, ce qui fait que la moitié de la largeur du processeur sert pour des opérations entières, et l'autre moitié pour les accès mémoire.
{|class="wikitable"
|-
! Port 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
|}
Les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon, sont un exemple moins représentatif. Ces microarchitectures utilisent le partage des ports d'émission au maximum. C'était des microarchitectures à triple émission, avec six ports d’émission. Il y avait trois ports d'émission pour les opérations flottantes, et trois ports d'émission pour les micro-opérations entières/mémoire. Il y avait trois ALU entières et trois unités mémoire, chaque ALU partageait un port d'émission avec une unité mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements.
{|class="wikitable"
|-
! Port 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 || ALU entière || FADD || FMUL || FPU (autres opérations)
|-
| Multiplieur || || || || ||
|-
| LOAD/STORE || LOAD/STORE || LOAD/STORE || || ||
|}
Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair.
Par exemple, la microarchitecture ''Golden Cove'' a deux ''barrel shifters''.
{|class="wikitable"
|-
! Port 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 !! Port d'émission n°7 !! Port d'émission n°8 !! Port d'émission n°9 !! Port d'émission n°10
|-
| ALU entière || ALU entière || ALU entière || ALU entière || ALU entière || LOAD || LOAD || LOAD || STORE || STORE
|-
| ''Barrel shifter'' || Multiplieur || Multiplieur || ''Barrel shifter'' || || || || || ||
|-
| Branchements || Diviseur || || Branchements || || || || || ||
|}
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les CPU superscalaire à exécution dans le désordre==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Les unités d'exécution dans le désordre===
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports de ''dispatch''. Mais avec des stations de réservation, il faut le double des ports de ''schedule''. La raison est que les registres sont lus après émission avec une fenê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 de ''schedule'' que de ''dispatch''.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de 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===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Avec plusieurs dizaines d'unités de calcul différentes, ajouter autant 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.
La première solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Le processeur détecte quand il n'y a pas assez de ports pour servir toutes les micro-opérations. Quand ça arrive, l'unité d'émission émet assez de micro-opérations pour exploiter les ports disponibles, mais met en attente les micro-opérations en trop, le temps que les ports se libèrent. Pour cela, le banc de registre est géré par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Une autre solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une dernière solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
==L'évolution historique de la superscalarité==
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. La plupart des CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 sont rapidement montés à de la quadruple émission, avec quelques exceptions. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 ||
|-
! 1995
| 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000
|-
! 1996
| class="f_bleu | 620 || || || || class="f_bleu | 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
===Les processeurs historiques à double émission===
Pour commencer, étudions le cas des premiers CPU superscalaires à double émission. Ils sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Notamment, la duplication des autres unités ne sert pour ainsi dire à rien, ce qui limite les possibilités, mais rendra les explications plus simples.
Il pouvaient se classer en trois types principaux, avec quelques processeurs qui ne rentraient pas dans ces cases. Le premier type utilise un ''partitionnement complet'' entre ALU, FPU et unité mémoire. Le second utilisait la double émission entière-flottante. Le troisième utilisait la double émission entière. Voici le tableau chronologique disant quel CPU utilisait quelle technique.
* Le partitionnement INT-FLOAT-MEM est en jaune.
* La double émission entière-flottante est en vert.
* La double émission entière est en rouge.
* Les CPU barrés utilisent la quadruple émission et seront vu dans la section suivante.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| class="f_vert | RS/6000 || || || class="f_rouge | i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| class="f_vert | RSC || class="f_vert | PA 7100 || SuperSPARC || || class="f_jaune | 21064 ||
|-
! 1993
| class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || ||
|-
! 1994
| <s>POWER 2</s> || PA 7200 || || || <s>21164</s> ||
|}
Les processeurs laissés de côté dans le tableau précédent ne rentrent pas dans les catégories précédentes.
Le PA 7200 est un PA-7100 auquel on aurait rajouté une ALU entière. Le processeur exécutait les multiplications entières dans la FPU, comme son prédécesseur. Les combinaisons autorisées étaient donc INT + INT, INT + MEM, INT + FLOAT/MUL, MEM + FLOAT/MUL.
Le SuperSPARC était un CPU triple émission, avec deux ALU entières, une FPU et une unité mémoire. Il avait de nombreuses contraintes d’appariement. Par exemple, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Une bonne partie de ces contraintes était lié au banc de registre, qui n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions. Il faut noter que la seconde ALU a été abandonnée lors du passage au CPU HyperSPARC, qui est de plus repassé en double émission, ce qui est une évolution à rebours de l'évolution des autres CPUs.
===Les processeurs historiques à quadruple émission===
C'est la même année , en 1994, que la quadruple émission est arrivée et que les CPU ont commencé à avoir de plus en plus d'unités de calcul. Même les designs restés en double émission avaient ajouté des unités de calcul. Il y avait alors trois stratégies pour la quadruple émission.
La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Les FPUs étaient soit dupliquées, soit scindées en deux en séparant l'additionneur du multiplieur flottant. Le POWER 2 dupliquait ses FPUs à la perfection, alors que l'21264 partitionnait la FPU. Les accès mémoire étaient traités comme des opérations entières, et le processeur pouvait : soit en faire un seul par cycle, soit en émettre deux, sous conditions (le cache devait être double port pour lire/écrire deux données à la fois, comme on le verra dans la suite du chapitre).
Les processeurs MIPS R8000 et R10000 allaient un peu plus loin, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités.
La seconde implémentation dupliquait l'ALU entière et combinait le tout avec un partitionnement INT-FLOAT-MEM. De quoi exécuter en même temps deux opérations entières, une opération flottante, et un accès mémoire. 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).
Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, avec au minimum deux ALU entières. Pour comprendre ce que je veux dire, prenons l'exemple de l'UltraSPARC. Il avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplier flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes. Le processeur pouvait émettre 4 instructions, tant qu'elles atterrissaient dans des unités différentes.
La même méthode peut être utilisée pour la double émission. Le Motorola 88110, sorti en 1991, était un CPU à double émission avec pas moins de 10 unités : deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise.
La morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes. Le tout est résumé dans le tableau ci-dessous, avec :
* en vert les CPU utilisant la première stratégie ;
* en rouge les CPU utilisant la seconde stratégie ;
* en jaune les CPU utilisant la troisième stratégie.
{|class="wikitable"
|+ Processeurs quadruple émission historiques
|-
! Année !! Processeur !! Unités
|-
! 1991
| class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques
|-
|-
! rowspan="2" | 1994
| class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'')
|-
| class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM
|-
! rowspan="3" | 1995
| class="f_jaune | PA 8000 || class="f_jaune | 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD
|-
| class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques
|-
| MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM
|-
! rowspan="2" | 1996
| class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM
|-
| class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL)
|}
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
e5xxfin557dhg2ursopm57lhm4lwdpw
773195
773194
2026-09-25T20:54:02Z
Mewtow
31375
/* L'évolution historique de la superscalarité */
773195
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===L'impact des contraintes d’appariement sur la performance===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour la suite du chapitre===
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==Le ''front-end'' des processeurs superscalaires==
Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple.
Intuitivement, charger, décoder et 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 certaines processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. 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
|}
Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs.
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===L'émission parallèle des instructions===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elle sont propagée au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage).
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
==Les processeurs implémentés avec plusieurs pipelines==
Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Vous vous dites que l'implémentation d'un processeur superscalaire n'est pas la même suivant que le CPU utilise l'exécution dans le désordre, ou non. Mais dans les faits, la distinction est plus subtile que ca. Pour l'expliquer, je vais utiliser une classification originale, qui sépare les processeurs superscalaires en trois sous-types. La distinction se fait sur la manière dont la superscalarité est implémentée en matériel.La classification que j'utilise distingue :
* Les processeurs avec plusieurs pipelines, sans exécution dans le désordre.
* Les processeurs superscalaires avec un pipeline dynamique.
* Les processeurs avec des fenêtres d'instruction décentralisées.
La distinction n'est pas qu'une question d'implémentation matérielle. Elle impacte aussi le ratio entre instructions chargées/décodées et instructions émises. Vous vous dites que les deux sont égales, mais ce n'est le cas que sur les processeurs avec N pipelines ! Sur les autres processeurs, il est possible de décoder 4 instructions, mais d'en émettre maximum 6 par cycle ! Et sur les CPU avec des fenêtres d'instruction décentralisées, il y a aussi une différence entre le nombre d'instructions qui sont ''dispatch'' et ''schedule''.
La catégorie la plus simple est celle des '''processeurs avec plusieurs pipelines'''. Ce sont des processeurs superscalaires sans exécution dans le désordre. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction.
Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, d'une FPU, éventuellement d'un second banc de registres, guère plus. Il permet d'exécuter une seconde opération de calcul, pas plus, ce qui est à l'origine de contraintes d'appariemment assez fortes. De plus, il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section.
===La double émission entière-flottante===
L'exemple le plus simple utilise un pipeline pour les opérations flottantes, séparé du pipeline originel. Pour bien expliquer les choses, prenons le processeur illustré ci-dessous. Un processeur a deux ''back-end'' séparés, un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par l'ALU entière, passent éventuellement dans l'unité mémoire, pour ensuite arriver à l'étape de ''writeback''. La FPU et le banc de registre flottant ont eux aussi leur propre ''back-end'' séparé.
[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]]
Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaqaue pipeline, à chaque cycle.
L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine.
Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants.
===La double émission entière===
Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait.
Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
==Les processeurs superscalaires dynamiques==
Les processeurs précédents, avec 2 pipelines, avaient une unité d'émission assez simple. Elle avait deux ports de décodage, et deux ports d'émission. Le nombre de ports de décodage et d'émission étaient identiques. Vous vous dites que c'est naturel, et intuitif, que c'est normal pour un processeur superscalaire. Mais en réalité, la majorité des processeurs superscalaires a plus de ports d'émission que de décodage. Expliquer pourquoi va cependant nous demander de parler de comment la superscalarité exploite les unités de calcul.
Posons-nous cette question : de combien d'unités un processeur superscalaire a besoin ? Les unités en question ne sont nécessairement des unité de calcul, l'unité mémoire et l'unité de branchement comptent aussi. La réponse dépend du nombre de voies. Si le ''front-end'' charge et décode un paquet de N instructions, exécuter ces N instructions simultanément demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section.
La réponse dépend aussi des contraintes d’appariement. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission parfaite devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement, ce qui ferait passer de 6 unités à 12. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, le cout en circuit encore plus prohibitif !
En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée, alors que la double émission parait plus adaptée.
Et c'est là que la différence entre ports d'émission et de décodage devient intéressante. L'idée est d'avoir une unité d'émission avec deux ports de décodage, et 4 port d'émission (un par unité). Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux micro-opérations tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu.
Les tout premiers CPU superscalaires étaient nombreux à utiliser cette technique. Les exemples classiques sont l'HyperSPARC, de l'alpha 21064 et du PowerPC 603. C'étaient des CPU double émission, avec une ALU entière, une unité mémoire et une FPU. Nous parlerons de '''partitionnement INT-FLOAT-MEM'''. Les contraintes d’appariement variaient grandement suivant le processeur.
Le PowerPC 603 implémentait une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur.
Avant de poursuivre, précisons que la FPU regroupe un additionneur flottant et un multiplieur flottant. Et ce fait peut être exploité pour approfondir le partitionnement. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions).
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple, facile à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. La raison est que le partitionnement permet d'émettre des paires, triplets, quadruplets de micro-opérations qui sont rares en pratique.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''.
Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, 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. Et enfin, il faut rajouter un port d'émission pour pouvoir émettre une micro-opération entière en plus.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle.
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
Reste à connecter les unités de calcul à l'unité d'émission (indirectement, car il y a des étages de pipeline entre les deux). Une implémentation basique utilisee un port d'émission par unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La présence d'un circuit multiplieur et d'un ''barrel shifter'' pose des problèmes pour le partitionnement. Il est possible d'avoir un port d'émission dédié au multiplieur, et un autre au ''barrel shifter'', qui sont séparés de ceux pour l'ALU entière. Il est alors possible d'émettre une opération entière, une multiplication et un décalage, simultanément. Nous parlerons de '''partitionnement entier''' pour désigner cette possibilité.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
Le partitionnement entier est cependant très rare, car le gain en performance serait trop faible au regard des couts. Quelques processeurs historiques l'utilisaient, mais ce n'est plus le cas de nos jours. La raison est que les multiplications et les décalages sont assez rares dans le code, ce qui fait que le port d'émission associés sont sous-utilisés. Et un port d'émission a un cout en circuit assez important. 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.
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul. L'exemple classique est justement lié aux circuits multiplieur et ''barrel shifters''. L'idée est que le multiplieur et le ''barrel shifter'' sont mis sur le même port d'émission qu'une ALU entière.
Prenons par exemple le processeur à double émission entière illustré ci-contre. Un port d'émission est relié à une ALU entière seule, et l'autre port d'émission alimente la seconde ALU entière et le multiplieur. La conséquence est que le processeur peut émettre deux opérations entières, dont une multiplication. Si on avait utilisé un port d'émission dédié au multiplieur, il aurait été possible d'émettre deux opérations entières et une multiplication. On perd donc une voie, mais on gagne en flexibilité et on économise des circuits.
[[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]]
En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais le partage des ports d'émission peut appliquer à n'importe quelle unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. 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.
La solution est de partager des ports d'émission, pour passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple relie une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites. Le CPU peut émettre 6 opérations entières simultanément, grâce à 6 ALU entières. Les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires mélangent les trois techniques===
Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autre unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents.
* Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU.
* La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants.
* La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables.
* Le '''partage des ports d'émission''' réduit le cout en circuit du CPU, pour des pertes de performances mineures si bien fait.
De nos jours, le partitionnement est utilisé en complément de la duplication des ALU entières, de la duplication des autres unités, et du partage des ports d'émission. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. Le partage des ports d'émission a lui aussi mis un peu de temps pour s'imposer.
Les CPU superscalaires tendent à mettre le paquet niveau opérations arithmétiques, mais pas forcément au point de remplir la totalité de la largeur du processeur. La règle est que pour N voies, le processeur dispose d'au moins N/2 ALU entières. Et N/2 est une borne basse, la plupart des CPU x86 ont environ tournent plutôt autour des 2/3, voire des 3/4. Attention, cela ne signifie pas que le processeur émet systématiquement N/2 opération entières, il peut par exemple remplacer des opérations entières par des opérations flottantes.
En général, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. La conséquence est que les processeurs ont commencé à dupliquer leurs unités mémoire dès qu'ils ont atteint la quadruple émission, pour les CPU grand public. Par exemple, la microarchitecture ''Golden Cove'' d'Intel dispose de cinq unités de calcul, trois unités mémoire (deux spécialisées dans les écritures, trois dans les lectures). La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire. Notez que le ratio est équilibré entre opérations entières et mémoire, avec soit égalité, soit un avantage pour les ALU entières.
Une autre régularité est que la largeur totale du processeur peut être occupée par des opérations entières/mémoire. Pour le dire autrement, sur un processeur quadruple émission, il pourra émettre un mix de 4 instructions entière/mémoire, avec zéro opération flottante. Idem avec des processeurs triples ou pentuple émission, voire plus. Si le processeur émet une opération flottante, elle remplace une opération entière.
Prenons l'exemple des 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. C'était des processeurs quadruple émission, ce qui fait que la moitié de la largeur du processeur sert pour des opérations entières, et l'autre moitié pour les accès mémoire.
{|class="wikitable"
|-
! Port 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
|}
Les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon, sont un exemple moins représentatif. Ces microarchitectures utilisent le partage des ports d'émission au maximum. C'était des microarchitectures à triple émission, avec six ports d’émission. Il y avait trois ports d'émission pour les opérations flottantes, et trois ports d'émission pour les micro-opérations entières/mémoire. Il y avait trois ALU entières et trois unités mémoire, chaque ALU partageait un port d'émission avec une unité mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements.
{|class="wikitable"
|-
! Port 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 || ALU entière || FADD || FMUL || FPU (autres opérations)
|-
| Multiplieur || || || || ||
|-
| LOAD/STORE || LOAD/STORE || LOAD/STORE || || ||
|}
Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair.
Par exemple, la microarchitecture ''Golden Cove'' a deux ''barrel shifters''.
{|class="wikitable"
|-
! Port 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 !! Port d'émission n°7 !! Port d'émission n°8 !! Port d'émission n°9 !! Port d'émission n°10
|-
| ALU entière || ALU entière || ALU entière || ALU entière || ALU entière || LOAD || LOAD || LOAD || STORE || STORE
|-
| ''Barrel shifter'' || Multiplieur || Multiplieur || ''Barrel shifter'' || || || || || ||
|-
| Branchements || Diviseur || || Branchements || || || || || ||
|}
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les CPU superscalaire à exécution dans le désordre==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Les unités d'exécution dans le désordre===
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports de ''dispatch''. Mais avec des stations de réservation, il faut le double des ports de ''schedule''. La raison est que les registres sont lus après émission avec une fenê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 de ''schedule'' que de ''dispatch''.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de 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===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Avec plusieurs dizaines d'unités de calcul différentes, ajouter autant 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.
La première solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Le processeur détecte quand il n'y a pas assez de ports pour servir toutes les micro-opérations. Quand ça arrive, l'unité d'émission émet assez de micro-opérations pour exploiter les ports disponibles, mais met en attente les micro-opérations en trop, le temps que les ports se libèrent. Pour cela, le banc de registre est géré par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Une autre solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une dernière solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
l5two93endljeslxw1vnil0cr5d6z1k
773196
773195
2026-09-25T20:54:14Z
Mewtow
31375
/* Les CPU superscalaires mélangent les trois techniques */
773196
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===L'impact des contraintes d’appariement sur la performance===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour la suite du chapitre===
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==Le ''front-end'' des processeurs superscalaires==
Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple.
Intuitivement, charger, décoder et 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 certaines processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. 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
|}
Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs.
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===L'émission parallèle des instructions===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elle sont propagée au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage).
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
==Les processeurs implémentés avec plusieurs pipelines==
Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Vous vous dites que l'implémentation d'un processeur superscalaire n'est pas la même suivant que le CPU utilise l'exécution dans le désordre, ou non. Mais dans les faits, la distinction est plus subtile que ca. Pour l'expliquer, je vais utiliser une classification originale, qui sépare les processeurs superscalaires en trois sous-types. La distinction se fait sur la manière dont la superscalarité est implémentée en matériel.La classification que j'utilise distingue :
* Les processeurs avec plusieurs pipelines, sans exécution dans le désordre.
* Les processeurs superscalaires avec un pipeline dynamique.
* Les processeurs avec des fenêtres d'instruction décentralisées.
La distinction n'est pas qu'une question d'implémentation matérielle. Elle impacte aussi le ratio entre instructions chargées/décodées et instructions émises. Vous vous dites que les deux sont égales, mais ce n'est le cas que sur les processeurs avec N pipelines ! Sur les autres processeurs, il est possible de décoder 4 instructions, mais d'en émettre maximum 6 par cycle ! Et sur les CPU avec des fenêtres d'instruction décentralisées, il y a aussi une différence entre le nombre d'instructions qui sont ''dispatch'' et ''schedule''.
La catégorie la plus simple est celle des '''processeurs avec plusieurs pipelines'''. Ce sont des processeurs superscalaires sans exécution dans le désordre. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction.
Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, d'une FPU, éventuellement d'un second banc de registres, guère plus. Il permet d'exécuter une seconde opération de calcul, pas plus, ce qui est à l'origine de contraintes d'appariemment assez fortes. De plus, il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section.
===La double émission entière-flottante===
L'exemple le plus simple utilise un pipeline pour les opérations flottantes, séparé du pipeline originel. Pour bien expliquer les choses, prenons le processeur illustré ci-dessous. Un processeur a deux ''back-end'' séparés, un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par l'ALU entière, passent éventuellement dans l'unité mémoire, pour ensuite arriver à l'étape de ''writeback''. La FPU et le banc de registre flottant ont eux aussi leur propre ''back-end'' séparé.
[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]]
Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaqaue pipeline, à chaque cycle.
L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine.
Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants.
===La double émission entière===
Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait.
Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
==Les processeurs superscalaires dynamiques==
Les processeurs précédents, avec 2 pipelines, avaient une unité d'émission assez simple. Elle avait deux ports de décodage, et deux ports d'émission. Le nombre de ports de décodage et d'émission étaient identiques. Vous vous dites que c'est naturel, et intuitif, que c'est normal pour un processeur superscalaire. Mais en réalité, la majorité des processeurs superscalaires a plus de ports d'émission que de décodage. Expliquer pourquoi va cependant nous demander de parler de comment la superscalarité exploite les unités de calcul.
Posons-nous cette question : de combien d'unités un processeur superscalaire a besoin ? Les unités en question ne sont nécessairement des unité de calcul, l'unité mémoire et l'unité de branchement comptent aussi. La réponse dépend du nombre de voies. Si le ''front-end'' charge et décode un paquet de N instructions, exécuter ces N instructions simultanément demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section.
La réponse dépend aussi des contraintes d’appariement. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission parfaite devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement, ce qui ferait passer de 6 unités à 12. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, le cout en circuit encore plus prohibitif !
En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée, alors que la double émission parait plus adaptée.
Et c'est là que la différence entre ports d'émission et de décodage devient intéressante. L'idée est d'avoir une unité d'émission avec deux ports de décodage, et 4 port d'émission (un par unité). Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux micro-opérations tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu.
Les tout premiers CPU superscalaires étaient nombreux à utiliser cette technique. Les exemples classiques sont l'HyperSPARC, de l'alpha 21064 et du PowerPC 603. C'étaient des CPU double émission, avec une ALU entière, une unité mémoire et une FPU. Nous parlerons de '''partitionnement INT-FLOAT-MEM'''. Les contraintes d’appariement variaient grandement suivant le processeur.
Le PowerPC 603 implémentait une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur.
Avant de poursuivre, précisons que la FPU regroupe un additionneur flottant et un multiplieur flottant. Et ce fait peut être exploité pour approfondir le partitionnement. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions).
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple, facile à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. La raison est que le partitionnement permet d'émettre des paires, triplets, quadruplets de micro-opérations qui sont rares en pratique.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''.
Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, 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. Et enfin, il faut rajouter un port d'émission pour pouvoir émettre une micro-opération entière en plus.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle.
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
Reste à connecter les unités de calcul à l'unité d'émission (indirectement, car il y a des étages de pipeline entre les deux). Une implémentation basique utilisee un port d'émission par unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La présence d'un circuit multiplieur et d'un ''barrel shifter'' pose des problèmes pour le partitionnement. Il est possible d'avoir un port d'émission dédié au multiplieur, et un autre au ''barrel shifter'', qui sont séparés de ceux pour l'ALU entière. Il est alors possible d'émettre une opération entière, une multiplication et un décalage, simultanément. Nous parlerons de '''partitionnement entier''' pour désigner cette possibilité.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
Le partitionnement entier est cependant très rare, car le gain en performance serait trop faible au regard des couts. Quelques processeurs historiques l'utilisaient, mais ce n'est plus le cas de nos jours. La raison est que les multiplications et les décalages sont assez rares dans le code, ce qui fait que le port d'émission associés sont sous-utilisés. Et un port d'émission a un cout en circuit assez important. 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.
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul. L'exemple classique est justement lié aux circuits multiplieur et ''barrel shifters''. L'idée est que le multiplieur et le ''barrel shifter'' sont mis sur le même port d'émission qu'une ALU entière.
Prenons par exemple le processeur à double émission entière illustré ci-contre. Un port d'émission est relié à une ALU entière seule, et l'autre port d'émission alimente la seconde ALU entière et le multiplieur. La conséquence est que le processeur peut émettre deux opérations entières, dont une multiplication. Si on avait utilisé un port d'émission dédié au multiplieur, il aurait été possible d'émettre deux opérations entières et une multiplication. On perd donc une voie, mais on gagne en flexibilité et on économise des circuits.
[[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]]
En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais le partage des ports d'émission peut appliquer à n'importe quelle unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. 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.
La solution est de partager des ports d'émission, pour passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple relie une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites. Le CPU peut émettre 6 opérations entières simultanément, grâce à 6 ALU entières. Les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires mélangent les trois techniques===
Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autre unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents.
* Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU.
* La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants.
* La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables.
* Le '''partage des ports d'émission''' réduit le cout en circuit du CPU, pour des pertes de performances mineures si bien fait.
De nos jours, le partitionnement est utilisé en complément de la duplication des ALU entières, de la duplication des autres unités, et du partage des ports d'émission. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. Le partage des ports d'émission a lui aussi mis un peu de temps pour s'imposer.
Les CPU superscalaires tendent à mettre le paquet niveau opérations arithmétiques, mais pas forcément au point de remplir la totalité de la largeur du processeur. La règle est que pour N voies, le processeur dispose d'au moins N/2 ALU entières. Et N/2 est une borne basse, la plupart des CPU x86 ont environ tournent plutôt autour des 2/3, voire des 3/4. Attention, cela ne signifie pas que le processeur émet systématiquement N/2 opération entières, il peut par exemple remplacer des opérations entières par des opérations flottantes.
En général, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. La conséquence est que les processeurs ont commencé à dupliquer leurs unités mémoire dès qu'ils ont atteint la quadruple émission, pour les CPU grand public. Par exemple, la microarchitecture ''Golden Cove'' d'Intel dispose de cinq unités de calcul, trois unités mémoire (deux spécialisées dans les écritures, trois dans les lectures). La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire. Notez que le ratio est équilibré entre opérations entières et mémoire, avec soit égalité, soit un avantage pour les ALU entières.
Une autre régularité est que la largeur totale du processeur peut être occupée par des opérations entières/mémoire. Pour le dire autrement, sur un processeur quadruple émission, il pourra émettre un mix de 4 instructions entière/mémoire, avec zéro opération flottante. Idem avec des processeurs triples ou pentuple émission, voire plus. Si le processeur émet une opération flottante, elle remplace une opération entière.
Prenons l'exemple des 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. C'était des processeurs quadruple émission, ce qui fait que la moitié de la largeur du processeur sert pour des opérations entières, et l'autre moitié pour les accès mémoire.
{|class="wikitable"
|-
! Port 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
|}
Les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon, sont un exemple moins représentatif. Ces microarchitectures utilisent le partage des ports d'émission au maximum. C'était des microarchitectures à triple émission, avec six ports d’émission. Il y avait trois ports d'émission pour les opérations flottantes, et trois ports d'émission pour les micro-opérations entières/mémoire. Il y avait trois ALU entières et trois unités mémoire, chaque ALU partageait un port d'émission avec une unité mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements.
{|class="wikitable"
|-
! Port 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 || ALU entière || FADD || FMUL || FPU (autres opérations)
|-
| Multiplieur || || || || ||
|-
| LOAD/STORE || LOAD/STORE || LOAD/STORE || || ||
|}
Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair.
Par exemple, la microarchitecture ''Golden Cove'' a deux ''barrel shifters''.
{|class="wikitable"
|-
! Port 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 !! Port d'émission n°7 !! Port d'émission n°8 !! Port d'émission n°9 !! Port d'émission n°10
|-
| ALU entière || ALU entière || ALU entière || ALU entière || ALU entière || LOAD || LOAD || LOAD || STORE || STORE
|-
| ''Barrel shifter'' || Multiplieur || Multiplieur || ''Barrel shifter'' || || || || || ||
|-
| Branchements || Diviseur || || Branchements || || || || || ||
|}
==L'évolution historique de la superscalarité==
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. La plupart des CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 sont rapidement montés à de la quadruple émission, avec quelques exceptions. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 ||
|-
! 1995
| 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000
|-
! 1996
| class="f_bleu | 620 || || || || class="f_bleu | 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
===Les processeurs historiques à double émission===
Pour commencer, étudions le cas des premiers CPU superscalaires à double émission. Ils sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Notamment, la duplication des autres unités ne sert pour ainsi dire à rien, ce qui limite les possibilités, mais rendra les explications plus simples.
Il pouvaient se classer en trois types principaux, avec quelques processeurs qui ne rentraient pas dans ces cases. Le premier type utilise un ''partitionnement complet'' entre ALU, FPU et unité mémoire. Le second utilisait la double émission entière-flottante. Le troisième utilisait la double émission entière. Voici le tableau chronologique disant quel CPU utilisait quelle technique.
* Le partitionnement INT-FLOAT-MEM est en jaune.
* La double émission entière-flottante est en vert.
* La double émission entière est en rouge.
* Les CPU barrés utilisent la quadruple émission et seront vu dans la section suivante.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| class="f_vert | RS/6000 || || || class="f_rouge | i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| class="f_vert | RSC || class="f_vert | PA 7100 || SuperSPARC || || class="f_jaune | 21064 ||
|-
! 1993
| class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || ||
|-
! 1994
| <s>POWER 2</s> || PA 7200 || || || <s>21164</s> ||
|}
Les processeurs laissés de côté dans le tableau précédent ne rentrent pas dans les catégories précédentes.
Le PA 7200 est un PA-7100 auquel on aurait rajouté une ALU entière. Le processeur exécutait les multiplications entières dans la FPU, comme son prédécesseur. Les combinaisons autorisées étaient donc INT + INT, INT + MEM, INT + FLOAT/MUL, MEM + FLOAT/MUL.
Le SuperSPARC était un CPU triple émission, avec deux ALU entières, une FPU et une unité mémoire. Il avait de nombreuses contraintes d’appariement. Par exemple, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Une bonne partie de ces contraintes était lié au banc de registre, qui n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions. Il faut noter que la seconde ALU a été abandonnée lors du passage au CPU HyperSPARC, qui est de plus repassé en double émission, ce qui est une évolution à rebours de l'évolution des autres CPUs.
===Les processeurs historiques à quadruple émission===
C'est la même année , en 1994, que la quadruple émission est arrivée et que les CPU ont commencé à avoir de plus en plus d'unités de calcul. Même les designs restés en double émission avaient ajouté des unités de calcul. Il y avait alors trois stratégies pour la quadruple émission.
La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Les FPUs étaient soit dupliquées, soit scindées en deux en séparant l'additionneur du multiplieur flottant. Le POWER 2 dupliquait ses FPUs à la perfection, alors que l'21264 partitionnait la FPU. Les accès mémoire étaient traités comme des opérations entières, et le processeur pouvait : soit en faire un seul par cycle, soit en émettre deux, sous conditions (le cache devait être double port pour lire/écrire deux données à la fois, comme on le verra dans la suite du chapitre).
Les processeurs MIPS R8000 et R10000 allaient un peu plus loin, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités.
La seconde implémentation dupliquait l'ALU entière et combinait le tout avec un partitionnement INT-FLOAT-MEM. De quoi exécuter en même temps deux opérations entières, une opération flottante, et un accès mémoire. 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).
Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, avec au minimum deux ALU entières. Pour comprendre ce que je veux dire, prenons l'exemple de l'UltraSPARC. Il avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplier flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes. Le processeur pouvait émettre 4 instructions, tant qu'elles atterrissaient dans des unités différentes.
La même méthode peut être utilisée pour la double émission. Le Motorola 88110, sorti en 1991, était un CPU à double émission avec pas moins de 10 unités : deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise.
La morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes. Le tout est résumé dans le tableau ci-dessous, avec :
* en vert les CPU utilisant la première stratégie ;
* en rouge les CPU utilisant la seconde stratégie ;
* en jaune les CPU utilisant la troisième stratégie.
{|class="wikitable"
|+ Processeurs quadruple émission historiques
|-
! Année !! Processeur !! Unités
|-
! 1991
| class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques
|-
|-
! rowspan="2" | 1994
| class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'')
|-
| class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM
|-
! rowspan="3" | 1995
| class="f_jaune | PA 8000 || class="f_jaune | 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD
|-
| class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques
|-
| MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM
|-
! rowspan="2" | 1996
| class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM
|-
| class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL)
|}
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les CPU superscalaire à exécution dans le désordre==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Les unités d'exécution dans le désordre===
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports de ''dispatch''. Mais avec des stations de réservation, il faut le double des ports de ''schedule''. La raison est que les registres sont lus après émission avec une fenê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 de ''schedule'' que de ''dispatch''.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de 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===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Avec plusieurs dizaines d'unités de calcul différentes, ajouter autant 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.
La première solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Le processeur détecte quand il n'y a pas assez de ports pour servir toutes les micro-opérations. Quand ça arrive, l'unité d'émission émet assez de micro-opérations pour exploiter les ports disponibles, mais met en attente les micro-opérations en trop, le temps que les ports se libèrent. Pour cela, le banc de registre est géré par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Une autre solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une dernière solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
0628emhafp5uhd0lcfjw6pyoqr8lmc0
773197
773196
2026-09-25T20:57:17Z
Mewtow
31375
/* Les processeurs historiques à double émission */
773197
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===L'impact des contraintes d’appariement sur la performance===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour la suite du chapitre===
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==Le ''front-end'' des processeurs superscalaires==
Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple.
Intuitivement, charger, décoder et 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 certaines processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. 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
|}
Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs.
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===L'émission parallèle des instructions===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elle sont propagée au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage).
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
==Les processeurs implémentés avec plusieurs pipelines==
Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Vous vous dites que l'implémentation d'un processeur superscalaire n'est pas la même suivant que le CPU utilise l'exécution dans le désordre, ou non. Mais dans les faits, la distinction est plus subtile que ca. Pour l'expliquer, je vais utiliser une classification originale, qui sépare les processeurs superscalaires en trois sous-types. La distinction se fait sur la manière dont la superscalarité est implémentée en matériel.La classification que j'utilise distingue :
* Les processeurs avec plusieurs pipelines, sans exécution dans le désordre.
* Les processeurs superscalaires avec un pipeline dynamique.
* Les processeurs avec des fenêtres d'instruction décentralisées.
La distinction n'est pas qu'une question d'implémentation matérielle. Elle impacte aussi le ratio entre instructions chargées/décodées et instructions émises. Vous vous dites que les deux sont égales, mais ce n'est le cas que sur les processeurs avec N pipelines ! Sur les autres processeurs, il est possible de décoder 4 instructions, mais d'en émettre maximum 6 par cycle ! Et sur les CPU avec des fenêtres d'instruction décentralisées, il y a aussi une différence entre le nombre d'instructions qui sont ''dispatch'' et ''schedule''.
La catégorie la plus simple est celle des '''processeurs avec plusieurs pipelines'''. Ce sont des processeurs superscalaires sans exécution dans le désordre. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction.
Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, d'une FPU, éventuellement d'un second banc de registres, guère plus. Il permet d'exécuter une seconde opération de calcul, pas plus, ce qui est à l'origine de contraintes d'appariemment assez fortes. De plus, il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section.
===La double émission entière-flottante===
L'exemple le plus simple utilise un pipeline pour les opérations flottantes, séparé du pipeline originel. Pour bien expliquer les choses, prenons le processeur illustré ci-dessous. Un processeur a deux ''back-end'' séparés, un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par l'ALU entière, passent éventuellement dans l'unité mémoire, pour ensuite arriver à l'étape de ''writeback''. La FPU et le banc de registre flottant ont eux aussi leur propre ''back-end'' séparé.
[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]]
Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaqaue pipeline, à chaque cycle.
L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine.
Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants.
===La double émission entière===
Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait.
Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
==Les processeurs superscalaires dynamiques==
Les processeurs précédents, avec 2 pipelines, avaient une unité d'émission assez simple. Elle avait deux ports de décodage, et deux ports d'émission. Le nombre de ports de décodage et d'émission étaient identiques. Vous vous dites que c'est naturel, et intuitif, que c'est normal pour un processeur superscalaire. Mais en réalité, la majorité des processeurs superscalaires a plus de ports d'émission que de décodage. Expliquer pourquoi va cependant nous demander de parler de comment la superscalarité exploite les unités de calcul.
Posons-nous cette question : de combien d'unités un processeur superscalaire a besoin ? Les unités en question ne sont nécessairement des unité de calcul, l'unité mémoire et l'unité de branchement comptent aussi. La réponse dépend du nombre de voies. Si le ''front-end'' charge et décode un paquet de N instructions, exécuter ces N instructions simultanément demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section.
La réponse dépend aussi des contraintes d’appariement. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission parfaite devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement, ce qui ferait passer de 6 unités à 12. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, le cout en circuit encore plus prohibitif !
En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée, alors que la double émission parait plus adaptée.
Et c'est là que la différence entre ports d'émission et de décodage devient intéressante. L'idée est d'avoir une unité d'émission avec deux ports de décodage, et 4 port d'émission (un par unité). Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux micro-opérations tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu.
Les tout premiers CPU superscalaires étaient nombreux à utiliser cette technique. Les exemples classiques sont l'HyperSPARC, de l'alpha 21064 et du PowerPC 603. C'étaient des CPU double émission, avec une ALU entière, une unité mémoire et une FPU. Nous parlerons de '''partitionnement INT-FLOAT-MEM'''. Les contraintes d’appariement variaient grandement suivant le processeur.
Le PowerPC 603 implémentait une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur.
Avant de poursuivre, précisons que la FPU regroupe un additionneur flottant et un multiplieur flottant. Et ce fait peut être exploité pour approfondir le partitionnement. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions).
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple, facile à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. La raison est que le partitionnement permet d'émettre des paires, triplets, quadruplets de micro-opérations qui sont rares en pratique.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''.
Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, 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. Et enfin, il faut rajouter un port d'émission pour pouvoir émettre une micro-opération entière en plus.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle.
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
Reste à connecter les unités de calcul à l'unité d'émission (indirectement, car il y a des étages de pipeline entre les deux). Une implémentation basique utilisee un port d'émission par unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La présence d'un circuit multiplieur et d'un ''barrel shifter'' pose des problèmes pour le partitionnement. Il est possible d'avoir un port d'émission dédié au multiplieur, et un autre au ''barrel shifter'', qui sont séparés de ceux pour l'ALU entière. Il est alors possible d'émettre une opération entière, une multiplication et un décalage, simultanément. Nous parlerons de '''partitionnement entier''' pour désigner cette possibilité.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
Le partitionnement entier est cependant très rare, car le gain en performance serait trop faible au regard des couts. Quelques processeurs historiques l'utilisaient, mais ce n'est plus le cas de nos jours. La raison est que les multiplications et les décalages sont assez rares dans le code, ce qui fait que le port d'émission associés sont sous-utilisés. Et un port d'émission a un cout en circuit assez important. 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.
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul. L'exemple classique est justement lié aux circuits multiplieur et ''barrel shifters''. L'idée est que le multiplieur et le ''barrel shifter'' sont mis sur le même port d'émission qu'une ALU entière.
Prenons par exemple le processeur à double émission entière illustré ci-contre. Un port d'émission est relié à une ALU entière seule, et l'autre port d'émission alimente la seconde ALU entière et le multiplieur. La conséquence est que le processeur peut émettre deux opérations entières, dont une multiplication. Si on avait utilisé un port d'émission dédié au multiplieur, il aurait été possible d'émettre deux opérations entières et une multiplication. On perd donc une voie, mais on gagne en flexibilité et on économise des circuits.
[[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]]
En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais le partage des ports d'émission peut appliquer à n'importe quelle unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. 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.
La solution est de partager des ports d'émission, pour passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple relie une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites. Le CPU peut émettre 6 opérations entières simultanément, grâce à 6 ALU entières. Les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires mélangent les trois techniques===
Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autre unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents.
* Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU.
* La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants.
* La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables.
* Le '''partage des ports d'émission''' réduit le cout en circuit du CPU, pour des pertes de performances mineures si bien fait.
De nos jours, le partitionnement est utilisé en complément de la duplication des ALU entières, de la duplication des autres unités, et du partage des ports d'émission. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. Le partage des ports d'émission a lui aussi mis un peu de temps pour s'imposer.
Les CPU superscalaires tendent à mettre le paquet niveau opérations arithmétiques, mais pas forcément au point de remplir la totalité de la largeur du processeur. La règle est que pour N voies, le processeur dispose d'au moins N/2 ALU entières. Et N/2 est une borne basse, la plupart des CPU x86 ont environ tournent plutôt autour des 2/3, voire des 3/4. Attention, cela ne signifie pas que le processeur émet systématiquement N/2 opération entières, il peut par exemple remplacer des opérations entières par des opérations flottantes.
En général, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. La conséquence est que les processeurs ont commencé à dupliquer leurs unités mémoire dès qu'ils ont atteint la quadruple émission, pour les CPU grand public. Par exemple, la microarchitecture ''Golden Cove'' d'Intel dispose de cinq unités de calcul, trois unités mémoire (deux spécialisées dans les écritures, trois dans les lectures). La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire. Notez que le ratio est équilibré entre opérations entières et mémoire, avec soit égalité, soit un avantage pour les ALU entières.
Une autre régularité est que la largeur totale du processeur peut être occupée par des opérations entières/mémoire. Pour le dire autrement, sur un processeur quadruple émission, il pourra émettre un mix de 4 instructions entière/mémoire, avec zéro opération flottante. Idem avec des processeurs triples ou pentuple émission, voire plus. Si le processeur émet une opération flottante, elle remplace une opération entière.
Prenons l'exemple des 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. C'était des processeurs quadruple émission, ce qui fait que la moitié de la largeur du processeur sert pour des opérations entières, et l'autre moitié pour les accès mémoire.
{|class="wikitable"
|-
! Port 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
|}
Les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon, sont un exemple moins représentatif. Ces microarchitectures utilisent le partage des ports d'émission au maximum. C'était des microarchitectures à triple émission, avec six ports d’émission. Il y avait trois ports d'émission pour les opérations flottantes, et trois ports d'émission pour les micro-opérations entières/mémoire. Il y avait trois ALU entières et trois unités mémoire, chaque ALU partageait un port d'émission avec une unité mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements.
{|class="wikitable"
|-
! Port 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 || ALU entière || FADD || FMUL || FPU (autres opérations)
|-
| Multiplieur || || || || ||
|-
| LOAD/STORE || LOAD/STORE || LOAD/STORE || || ||
|}
Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair.
Par exemple, la microarchitecture ''Golden Cove'' a deux ''barrel shifters''.
{|class="wikitable"
|-
! Port 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 !! Port d'émission n°7 !! Port d'émission n°8 !! Port d'émission n°9 !! Port d'émission n°10
|-
| ALU entière || ALU entière || ALU entière || ALU entière || ALU entière || LOAD || LOAD || LOAD || STORE || STORE
|-
| ''Barrel shifter'' || Multiplieur || Multiplieur || ''Barrel shifter'' || || || || || ||
|-
| Branchements || Diviseur || || Branchements || || || || || ||
|}
==L'évolution historique de la superscalarité==
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. La plupart des CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 sont rapidement montés à de la quadruple émission, avec quelques exceptions. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 ||
|-
! 1995
| 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000
|-
! 1996
| class="f_bleu | 620 || || || || class="f_bleu | 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
===Les processeurs historiques à double émission===
Les premiers CPU à double émission sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Il pouvaient se classer en trois types principaux, avec quelques processeurs qui ne rentraient pas dans ces cases.
* Le premier type utilise un ''partitionnement complet'' entre ALU, FPU et unité mémoire.
* Le second utilisait la double émission entière-flottante.
* Le troisième utilisait la double émission entière.
* Le quatrième utilisait un grand nombre d'unités, avec beaucoup de ports d'émission.
Voici le tableau chronologique disant quel CPU utilisait quelle technique.
* Le partitionnement INT-FLOAT-MEM est en jaune.
* La double émission entière-flottante est en vert.
* La double émission entière est en rouge.
* Les CPUs avec beaucoup d'unités ne sont pas coloriés.
* Les CPU barrés utilisent la quadruple émission et seront vu dans la section suivante.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| class="f_vert | RS/6000 || || || class="f_rouge | i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| class="f_vert | RSC || class="f_vert | PA 7100 || SuperSPARC || || class="f_jaune | 21064 ||
|-
! 1993
| class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || ||
|-
! 1994
| <s>POWER 2</s> || PA 7200 || || || <s>21164</s> ||
|}
Les processeurs laissés de côté dans le tableau précédent ne rentrent pas dans les catégories précédentes.
Le PA 7200 est un PA-7100 auquel on aurait rajouté une ALU entière. Le processeur exécutait les multiplications entières dans la FPU, comme son prédécesseur. Les combinaisons autorisées étaient donc INT + INT, INT + MEM, INT + FLOAT/MUL, MEM + FLOAT/MUL.
Le SuperSPARC était un CPU triple émission, avec deux ALU entières, une FPU et une unité mémoire. Il avait de nombreuses contraintes d’appariement. Par exemple, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Une bonne partie de ces contraintes était lié au banc de registre, qui n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions. Il faut noter que la seconde ALU a été abandonnée lors du passage au CPU HyperSPARC, qui est de plus repassé en double émission, ce qui est une évolution à rebours de l'évolution des autres CPUs.
===Les processeurs historiques à quadruple émission===
C'est la même année , en 1994, que la quadruple émission est arrivée et que les CPU ont commencé à avoir de plus en plus d'unités de calcul. Même les designs restés en double émission avaient ajouté des unités de calcul. Il y avait alors trois stratégies pour la quadruple émission.
La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Les FPUs étaient soit dupliquées, soit scindées en deux en séparant l'additionneur du multiplieur flottant. Le POWER 2 dupliquait ses FPUs à la perfection, alors que l'21264 partitionnait la FPU. Les accès mémoire étaient traités comme des opérations entières, et le processeur pouvait : soit en faire un seul par cycle, soit en émettre deux, sous conditions (le cache devait être double port pour lire/écrire deux données à la fois, comme on le verra dans la suite du chapitre).
Les processeurs MIPS R8000 et R10000 allaient un peu plus loin, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités.
La seconde implémentation dupliquait l'ALU entière et combinait le tout avec un partitionnement INT-FLOAT-MEM. De quoi exécuter en même temps deux opérations entières, une opération flottante, et un accès mémoire. 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).
Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, avec au minimum deux ALU entières. Pour comprendre ce que je veux dire, prenons l'exemple de l'UltraSPARC. Il avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplier flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes. Le processeur pouvait émettre 4 instructions, tant qu'elles atterrissaient dans des unités différentes.
La même méthode peut être utilisée pour la double émission. Le Motorola 88110, sorti en 1991, était un CPU à double émission avec pas moins de 10 unités : deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise.
La morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes. Le tout est résumé dans le tableau ci-dessous, avec :
* en vert les CPU utilisant la première stratégie ;
* en rouge les CPU utilisant la seconde stratégie ;
* en jaune les CPU utilisant la troisième stratégie.
{|class="wikitable"
|+ Processeurs quadruple émission historiques
|-
! Année !! Processeur !! Unités
|-
! 1991
| class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques
|-
|-
! rowspan="2" | 1994
| class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'')
|-
| class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM
|-
! rowspan="3" | 1995
| class="f_jaune | PA 8000 || class="f_jaune | 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD
|-
| class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques
|-
| MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM
|-
! rowspan="2" | 1996
| class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM
|-
| class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL)
|}
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les CPU superscalaire à exécution dans le désordre==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Les unités d'exécution dans le désordre===
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports de ''dispatch''. Mais avec des stations de réservation, il faut le double des ports de ''schedule''. La raison est que les registres sont lus après émission avec une fenê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 de ''schedule'' que de ''dispatch''.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de 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===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Avec plusieurs dizaines d'unités de calcul différentes, ajouter autant 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.
La première solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Le processeur détecte quand il n'y a pas assez de ports pour servir toutes les micro-opérations. Quand ça arrive, l'unité d'émission émet assez de micro-opérations pour exploiter les ports disponibles, mais met en attente les micro-opérations en trop, le temps que les ports se libèrent. Pour cela, le banc de registre est géré par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Une autre solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une dernière solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
hkrmmnpqp97ymai1c21zmv395bxe4pu
773198
773197
2026-09-25T20:58:30Z
Mewtow
31375
/* Les processeurs historiques à double émission */
773198
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===L'impact des contraintes d’appariement sur la performance===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour la suite du chapitre===
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==Le ''front-end'' des processeurs superscalaires==
Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple.
Intuitivement, charger, décoder et 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 certaines processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. 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
|}
Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs.
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===L'émission parallèle des instructions===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elle sont propagée au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage).
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
==Les processeurs implémentés avec plusieurs pipelines==
Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Vous vous dites que l'implémentation d'un processeur superscalaire n'est pas la même suivant que le CPU utilise l'exécution dans le désordre, ou non. Mais dans les faits, la distinction est plus subtile que ca. Pour l'expliquer, je vais utiliser une classification originale, qui sépare les processeurs superscalaires en trois sous-types. La distinction se fait sur la manière dont la superscalarité est implémentée en matériel.La classification que j'utilise distingue :
* Les processeurs avec plusieurs pipelines, sans exécution dans le désordre.
* Les processeurs superscalaires avec un pipeline dynamique.
* Les processeurs avec des fenêtres d'instruction décentralisées.
La distinction n'est pas qu'une question d'implémentation matérielle. Elle impacte aussi le ratio entre instructions chargées/décodées et instructions émises. Vous vous dites que les deux sont égales, mais ce n'est le cas que sur les processeurs avec N pipelines ! Sur les autres processeurs, il est possible de décoder 4 instructions, mais d'en émettre maximum 6 par cycle ! Et sur les CPU avec des fenêtres d'instruction décentralisées, il y a aussi une différence entre le nombre d'instructions qui sont ''dispatch'' et ''schedule''.
La catégorie la plus simple est celle des '''processeurs avec plusieurs pipelines'''. Ce sont des processeurs superscalaires sans exécution dans le désordre. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction.
Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, d'une FPU, éventuellement d'un second banc de registres, guère plus. Il permet d'exécuter une seconde opération de calcul, pas plus, ce qui est à l'origine de contraintes d'appariemment assez fortes. De plus, il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section.
===La double émission entière-flottante===
L'exemple le plus simple utilise un pipeline pour les opérations flottantes, séparé du pipeline originel. Pour bien expliquer les choses, prenons le processeur illustré ci-dessous. Un processeur a deux ''back-end'' séparés, un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par l'ALU entière, passent éventuellement dans l'unité mémoire, pour ensuite arriver à l'étape de ''writeback''. La FPU et le banc de registre flottant ont eux aussi leur propre ''back-end'' séparé.
[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]]
Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaqaue pipeline, à chaque cycle.
L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine.
Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants.
===La double émission entière===
Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait.
Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
==Les processeurs superscalaires dynamiques==
Les processeurs précédents, avec 2 pipelines, avaient une unité d'émission assez simple. Elle avait deux ports de décodage, et deux ports d'émission. Le nombre de ports de décodage et d'émission étaient identiques. Vous vous dites que c'est naturel, et intuitif, que c'est normal pour un processeur superscalaire. Mais en réalité, la majorité des processeurs superscalaires a plus de ports d'émission que de décodage. Expliquer pourquoi va cependant nous demander de parler de comment la superscalarité exploite les unités de calcul.
Posons-nous cette question : de combien d'unités un processeur superscalaire a besoin ? Les unités en question ne sont nécessairement des unité de calcul, l'unité mémoire et l'unité de branchement comptent aussi. La réponse dépend du nombre de voies. Si le ''front-end'' charge et décode un paquet de N instructions, exécuter ces N instructions simultanément demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section.
La réponse dépend aussi des contraintes d’appariement. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission parfaite devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement, ce qui ferait passer de 6 unités à 12. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, le cout en circuit encore plus prohibitif !
En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée, alors que la double émission parait plus adaptée.
Et c'est là que la différence entre ports d'émission et de décodage devient intéressante. L'idée est d'avoir une unité d'émission avec deux ports de décodage, et 4 port d'émission (un par unité). Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux micro-opérations tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu.
Les tout premiers CPU superscalaires étaient nombreux à utiliser cette technique. Les exemples classiques sont l'HyperSPARC, de l'alpha 21064 et du PowerPC 603. C'étaient des CPU double émission, avec une ALU entière, une unité mémoire et une FPU. Nous parlerons de '''partitionnement INT-FLOAT-MEM'''. Les contraintes d’appariement variaient grandement suivant le processeur.
Le PowerPC 603 implémentait une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur.
Avant de poursuivre, précisons que la FPU regroupe un additionneur flottant et un multiplieur flottant. Et ce fait peut être exploité pour approfondir le partitionnement. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions).
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple, facile à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. La raison est que le partitionnement permet d'émettre des paires, triplets, quadruplets de micro-opérations qui sont rares en pratique.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''.
Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, 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. Et enfin, il faut rajouter un port d'émission pour pouvoir émettre une micro-opération entière en plus.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle.
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
Reste à connecter les unités de calcul à l'unité d'émission (indirectement, car il y a des étages de pipeline entre les deux). Une implémentation basique utilisee un port d'émission par unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La présence d'un circuit multiplieur et d'un ''barrel shifter'' pose des problèmes pour le partitionnement. Il est possible d'avoir un port d'émission dédié au multiplieur, et un autre au ''barrel shifter'', qui sont séparés de ceux pour l'ALU entière. Il est alors possible d'émettre une opération entière, une multiplication et un décalage, simultanément. Nous parlerons de '''partitionnement entier''' pour désigner cette possibilité.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
Le partitionnement entier est cependant très rare, car le gain en performance serait trop faible au regard des couts. Quelques processeurs historiques l'utilisaient, mais ce n'est plus le cas de nos jours. La raison est que les multiplications et les décalages sont assez rares dans le code, ce qui fait que le port d'émission associés sont sous-utilisés. Et un port d'émission a un cout en circuit assez important. 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.
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul. L'exemple classique est justement lié aux circuits multiplieur et ''barrel shifters''. L'idée est que le multiplieur et le ''barrel shifter'' sont mis sur le même port d'émission qu'une ALU entière.
Prenons par exemple le processeur à double émission entière illustré ci-contre. Un port d'émission est relié à une ALU entière seule, et l'autre port d'émission alimente la seconde ALU entière et le multiplieur. La conséquence est que le processeur peut émettre deux opérations entières, dont une multiplication. Si on avait utilisé un port d'émission dédié au multiplieur, il aurait été possible d'émettre deux opérations entières et une multiplication. On perd donc une voie, mais on gagne en flexibilité et on économise des circuits.
[[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]]
En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais le partage des ports d'émission peut appliquer à n'importe quelle unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. 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.
La solution est de partager des ports d'émission, pour passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple relie une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites. Le CPU peut émettre 6 opérations entières simultanément, grâce à 6 ALU entières. Les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires mélangent les trois techniques===
Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autre unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents.
* Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU.
* La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants.
* La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables.
* Le '''partage des ports d'émission''' réduit le cout en circuit du CPU, pour des pertes de performances mineures si bien fait.
De nos jours, le partitionnement est utilisé en complément de la duplication des ALU entières, de la duplication des autres unités, et du partage des ports d'émission. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. Le partage des ports d'émission a lui aussi mis un peu de temps pour s'imposer.
Les CPU superscalaires tendent à mettre le paquet niveau opérations arithmétiques, mais pas forcément au point de remplir la totalité de la largeur du processeur. La règle est que pour N voies, le processeur dispose d'au moins N/2 ALU entières. Et N/2 est une borne basse, la plupart des CPU x86 ont environ tournent plutôt autour des 2/3, voire des 3/4. Attention, cela ne signifie pas que le processeur émet systématiquement N/2 opération entières, il peut par exemple remplacer des opérations entières par des opérations flottantes.
En général, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. La conséquence est que les processeurs ont commencé à dupliquer leurs unités mémoire dès qu'ils ont atteint la quadruple émission, pour les CPU grand public. Par exemple, la microarchitecture ''Golden Cove'' d'Intel dispose de cinq unités de calcul, trois unités mémoire (deux spécialisées dans les écritures, trois dans les lectures). La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire. Notez que le ratio est équilibré entre opérations entières et mémoire, avec soit égalité, soit un avantage pour les ALU entières.
Une autre régularité est que la largeur totale du processeur peut être occupée par des opérations entières/mémoire. Pour le dire autrement, sur un processeur quadruple émission, il pourra émettre un mix de 4 instructions entière/mémoire, avec zéro opération flottante. Idem avec des processeurs triples ou pentuple émission, voire plus. Si le processeur émet une opération flottante, elle remplace une opération entière.
Prenons l'exemple des 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. C'était des processeurs quadruple émission, ce qui fait que la moitié de la largeur du processeur sert pour des opérations entières, et l'autre moitié pour les accès mémoire.
{|class="wikitable"
|-
! Port 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
|}
Les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon, sont un exemple moins représentatif. Ces microarchitectures utilisent le partage des ports d'émission au maximum. C'était des microarchitectures à triple émission, avec six ports d’émission. Il y avait trois ports d'émission pour les opérations flottantes, et trois ports d'émission pour les micro-opérations entières/mémoire. Il y avait trois ALU entières et trois unités mémoire, chaque ALU partageait un port d'émission avec une unité mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements.
{|class="wikitable"
|-
! Port 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 || ALU entière || FADD || FMUL || FPU (autres opérations)
|-
| Multiplieur || || || || ||
|-
| LOAD/STORE || LOAD/STORE || LOAD/STORE || || ||
|}
Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair.
Par exemple, la microarchitecture ''Golden Cove'' a deux ''barrel shifters''.
{|class="wikitable"
|-
! Port 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 !! Port d'émission n°7 !! Port d'émission n°8 !! Port d'émission n°9 !! Port d'émission n°10
|-
| ALU entière || ALU entière || ALU entière || ALU entière || ALU entière || LOAD || LOAD || LOAD || STORE || STORE
|-
| ''Barrel shifter'' || Multiplieur || Multiplieur || ''Barrel shifter'' || || || || || ||
|-
| Branchements || Diviseur || || Branchements || || || || || ||
|}
==L'évolution historique de la superscalarité==
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. La plupart des CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 sont rapidement montés à de la quadruple émission, avec quelques exceptions. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 ||
|-
! 1995
| 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000
|-
! 1996
| class="f_bleu | 620 || || || || class="f_bleu | 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
===Les processeurs historiques à double émission===
Les premiers CPU à double émission sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Voici le tableau chronologique disant quel CPU utilisait quelle technique.
* Les CPU en jaune utilisaient un ''partitionnement complet'' entre ALU, FPU et unité mémoire.
* Les CPU en vert utilisaient la double émission entière-flottante.
* Les CPU en rouge utilisaient la double émission entière.
* Les CPU sans couleur utilisaient un grand nombre d'unités, avec beaucoup de ports d'émission.
* Les CPU barrés utilisent la quadruple émission et seront vu dans la section suivante.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| class="f_vert | RS/6000 || || || class="f_rouge | i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| class="f_vert | RSC || class="f_vert | PA 7100 || SuperSPARC || || class="f_jaune | 21064 ||
|-
! 1993
| class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || ||
|-
! 1994
| <s>POWER 2</s> || PA 7200 || || || <s>21164</s> ||
|}
Les processeurs laissés de côté dans le tableau précédent ne rentrent pas dans les catégories précédentes.
Le PA 7200 est un PA-7100 auquel on aurait rajouté une ALU entière. Le processeur exécutait les multiplications entières dans la FPU, comme son prédécesseur. Les combinaisons autorisées étaient donc INT + INT, INT + MEM, INT + FLOAT/MUL, MEM + FLOAT/MUL.
Le SuperSPARC était un CPU triple émission, avec deux ALU entières, une FPU et une unité mémoire. Il avait de nombreuses contraintes d’appariement. Par exemple, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Une bonne partie de ces contraintes était lié au banc de registre, qui n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions. Il faut noter que la seconde ALU a été abandonnée lors du passage au CPU HyperSPARC, qui est de plus repassé en double émission, ce qui est une évolution à rebours de l'évolution des autres CPUs.
===Les processeurs historiques à quadruple émission===
C'est la même année , en 1994, que la quadruple émission est arrivée et que les CPU ont commencé à avoir de plus en plus d'unités de calcul. Même les designs restés en double émission avaient ajouté des unités de calcul. Il y avait alors trois stratégies pour la quadruple émission.
La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Les FPUs étaient soit dupliquées, soit scindées en deux en séparant l'additionneur du multiplieur flottant. Le POWER 2 dupliquait ses FPUs à la perfection, alors que l'21264 partitionnait la FPU. Les accès mémoire étaient traités comme des opérations entières, et le processeur pouvait : soit en faire un seul par cycle, soit en émettre deux, sous conditions (le cache devait être double port pour lire/écrire deux données à la fois, comme on le verra dans la suite du chapitre).
Les processeurs MIPS R8000 et R10000 allaient un peu plus loin, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités.
La seconde implémentation dupliquait l'ALU entière et combinait le tout avec un partitionnement INT-FLOAT-MEM. De quoi exécuter en même temps deux opérations entières, une opération flottante, et un accès mémoire. 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).
Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, avec au minimum deux ALU entières. Pour comprendre ce que je veux dire, prenons l'exemple de l'UltraSPARC. Il avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplier flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes. Le processeur pouvait émettre 4 instructions, tant qu'elles atterrissaient dans des unités différentes.
La même méthode peut être utilisée pour la double émission. Le Motorola 88110, sorti en 1991, était un CPU à double émission avec pas moins de 10 unités : deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise.
La morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes. Le tout est résumé dans le tableau ci-dessous, avec :
* en vert les CPU utilisant la première stratégie ;
* en rouge les CPU utilisant la seconde stratégie ;
* en jaune les CPU utilisant la troisième stratégie.
{|class="wikitable"
|+ Processeurs quadruple émission historiques
|-
! Année !! Processeur !! Unités
|-
! 1991
| class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques
|-
|-
! rowspan="2" | 1994
| class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'')
|-
| class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM
|-
! rowspan="3" | 1995
| class="f_jaune | PA 8000 || class="f_jaune | 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD
|-
| class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques
|-
| MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM
|-
! rowspan="2" | 1996
| class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM
|-
| class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL)
|}
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les CPU superscalaire à exécution dans le désordre==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Les unités d'exécution dans le désordre===
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports de ''dispatch''. Mais avec des stations de réservation, il faut le double des ports de ''schedule''. La raison est que les registres sont lus après émission avec une fenê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 de ''schedule'' que de ''dispatch''.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de 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===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Avec plusieurs dizaines d'unités de calcul différentes, ajouter autant 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.
La première solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Le processeur détecte quand il n'y a pas assez de ports pour servir toutes les micro-opérations. Quand ça arrive, l'unité d'émission émet assez de micro-opérations pour exploiter les ports disponibles, mais met en attente les micro-opérations en trop, le temps que les ports se libèrent. Pour cela, le banc de registre est géré par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Une autre solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une dernière solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
gee8ps4o6pt6s580q9cnpzo0w2m147a
773199
773198
2026-09-25T20:59:11Z
Mewtow
31375
/* Les processeurs historiques à double émission */
773199
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===L'impact des contraintes d’appariement sur la performance===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour la suite du chapitre===
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==Le ''front-end'' des processeurs superscalaires==
Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple.
Intuitivement, charger, décoder et 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 certaines processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. 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
|}
Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs.
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===L'émission parallèle des instructions===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elle sont propagée au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage).
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
==Les processeurs implémentés avec plusieurs pipelines==
Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Vous vous dites que l'implémentation d'un processeur superscalaire n'est pas la même suivant que le CPU utilise l'exécution dans le désordre, ou non. Mais dans les faits, la distinction est plus subtile que ca. Pour l'expliquer, je vais utiliser une classification originale, qui sépare les processeurs superscalaires en trois sous-types. La distinction se fait sur la manière dont la superscalarité est implémentée en matériel.La classification que j'utilise distingue :
* Les processeurs avec plusieurs pipelines, sans exécution dans le désordre.
* Les processeurs superscalaires avec un pipeline dynamique.
* Les processeurs avec des fenêtres d'instruction décentralisées.
La distinction n'est pas qu'une question d'implémentation matérielle. Elle impacte aussi le ratio entre instructions chargées/décodées et instructions émises. Vous vous dites que les deux sont égales, mais ce n'est le cas que sur les processeurs avec N pipelines ! Sur les autres processeurs, il est possible de décoder 4 instructions, mais d'en émettre maximum 6 par cycle ! Et sur les CPU avec des fenêtres d'instruction décentralisées, il y a aussi une différence entre le nombre d'instructions qui sont ''dispatch'' et ''schedule''.
La catégorie la plus simple est celle des '''processeurs avec plusieurs pipelines'''. Ce sont des processeurs superscalaires sans exécution dans le désordre. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction.
Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, d'une FPU, éventuellement d'un second banc de registres, guère plus. Il permet d'exécuter une seconde opération de calcul, pas plus, ce qui est à l'origine de contraintes d'appariemment assez fortes. De plus, il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section.
===La double émission entière-flottante===
L'exemple le plus simple utilise un pipeline pour les opérations flottantes, séparé du pipeline originel. Pour bien expliquer les choses, prenons le processeur illustré ci-dessous. Un processeur a deux ''back-end'' séparés, un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par l'ALU entière, passent éventuellement dans l'unité mémoire, pour ensuite arriver à l'étape de ''writeback''. La FPU et le banc de registre flottant ont eux aussi leur propre ''back-end'' séparé.
[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]]
Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaqaue pipeline, à chaque cycle.
L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine.
Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants.
===La double émission entière===
Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait.
Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
==Les processeurs superscalaires dynamiques==
Les processeurs précédents, avec 2 pipelines, avaient une unité d'émission assez simple. Elle avait deux ports de décodage, et deux ports d'émission. Le nombre de ports de décodage et d'émission étaient identiques. Vous vous dites que c'est naturel, et intuitif, que c'est normal pour un processeur superscalaire. Mais en réalité, la majorité des processeurs superscalaires a plus de ports d'émission que de décodage. Expliquer pourquoi va cependant nous demander de parler de comment la superscalarité exploite les unités de calcul.
Posons-nous cette question : de combien d'unités un processeur superscalaire a besoin ? Les unités en question ne sont nécessairement des unité de calcul, l'unité mémoire et l'unité de branchement comptent aussi. La réponse dépend du nombre de voies. Si le ''front-end'' charge et décode un paquet de N instructions, exécuter ces N instructions simultanément demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section.
La réponse dépend aussi des contraintes d’appariement. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission parfaite devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement, ce qui ferait passer de 6 unités à 12. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, le cout en circuit encore plus prohibitif !
En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée, alors que la double émission parait plus adaptée.
Et c'est là que la différence entre ports d'émission et de décodage devient intéressante. L'idée est d'avoir une unité d'émission avec deux ports de décodage, et 4 port d'émission (un par unité). Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux micro-opérations tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu.
Les tout premiers CPU superscalaires étaient nombreux à utiliser cette technique. Les exemples classiques sont l'HyperSPARC, de l'alpha 21064 et du PowerPC 603. C'étaient des CPU double émission, avec une ALU entière, une unité mémoire et une FPU. Nous parlerons de '''partitionnement INT-FLOAT-MEM'''. Les contraintes d’appariement variaient grandement suivant le processeur.
Le PowerPC 603 implémentait une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur.
Avant de poursuivre, précisons que la FPU regroupe un additionneur flottant et un multiplieur flottant. Et ce fait peut être exploité pour approfondir le partitionnement. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions).
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple, facile à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. La raison est que le partitionnement permet d'émettre des paires, triplets, quadruplets de micro-opérations qui sont rares en pratique.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''.
Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, 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. Et enfin, il faut rajouter un port d'émission pour pouvoir émettre une micro-opération entière en plus.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle.
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
Reste à connecter les unités de calcul à l'unité d'émission (indirectement, car il y a des étages de pipeline entre les deux). Une implémentation basique utilisee un port d'émission par unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La présence d'un circuit multiplieur et d'un ''barrel shifter'' pose des problèmes pour le partitionnement. Il est possible d'avoir un port d'émission dédié au multiplieur, et un autre au ''barrel shifter'', qui sont séparés de ceux pour l'ALU entière. Il est alors possible d'émettre une opération entière, une multiplication et un décalage, simultanément. Nous parlerons de '''partitionnement entier''' pour désigner cette possibilité.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
Le partitionnement entier est cependant très rare, car le gain en performance serait trop faible au regard des couts. Quelques processeurs historiques l'utilisaient, mais ce n'est plus le cas de nos jours. La raison est que les multiplications et les décalages sont assez rares dans le code, ce qui fait que le port d'émission associés sont sous-utilisés. Et un port d'émission a un cout en circuit assez important. 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.
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul. L'exemple classique est justement lié aux circuits multiplieur et ''barrel shifters''. L'idée est que le multiplieur et le ''barrel shifter'' sont mis sur le même port d'émission qu'une ALU entière.
Prenons par exemple le processeur à double émission entière illustré ci-contre. Un port d'émission est relié à une ALU entière seule, et l'autre port d'émission alimente la seconde ALU entière et le multiplieur. La conséquence est que le processeur peut émettre deux opérations entières, dont une multiplication. Si on avait utilisé un port d'émission dédié au multiplieur, il aurait été possible d'émettre deux opérations entières et une multiplication. On perd donc une voie, mais on gagne en flexibilité et on économise des circuits.
[[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]]
En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais le partage des ports d'émission peut appliquer à n'importe quelle unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. 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.
La solution est de partager des ports d'émission, pour passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple relie une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites. Le CPU peut émettre 6 opérations entières simultanément, grâce à 6 ALU entières. Les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires mélangent les trois techniques===
Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autre unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents.
* Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU.
* La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants.
* La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables.
* Le '''partage des ports d'émission''' réduit le cout en circuit du CPU, pour des pertes de performances mineures si bien fait.
De nos jours, le partitionnement est utilisé en complément de la duplication des ALU entières, de la duplication des autres unités, et du partage des ports d'émission. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. Le partage des ports d'émission a lui aussi mis un peu de temps pour s'imposer.
Les CPU superscalaires tendent à mettre le paquet niveau opérations arithmétiques, mais pas forcément au point de remplir la totalité de la largeur du processeur. La règle est que pour N voies, le processeur dispose d'au moins N/2 ALU entières. Et N/2 est une borne basse, la plupart des CPU x86 ont environ tournent plutôt autour des 2/3, voire des 3/4. Attention, cela ne signifie pas que le processeur émet systématiquement N/2 opération entières, il peut par exemple remplacer des opérations entières par des opérations flottantes.
En général, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. La conséquence est que les processeurs ont commencé à dupliquer leurs unités mémoire dès qu'ils ont atteint la quadruple émission, pour les CPU grand public. Par exemple, la microarchitecture ''Golden Cove'' d'Intel dispose de cinq unités de calcul, trois unités mémoire (deux spécialisées dans les écritures, trois dans les lectures). La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire. Notez que le ratio est équilibré entre opérations entières et mémoire, avec soit égalité, soit un avantage pour les ALU entières.
Une autre régularité est que la largeur totale du processeur peut être occupée par des opérations entières/mémoire. Pour le dire autrement, sur un processeur quadruple émission, il pourra émettre un mix de 4 instructions entière/mémoire, avec zéro opération flottante. Idem avec des processeurs triples ou pentuple émission, voire plus. Si le processeur émet une opération flottante, elle remplace une opération entière.
Prenons l'exemple des 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. C'était des processeurs quadruple émission, ce qui fait que la moitié de la largeur du processeur sert pour des opérations entières, et l'autre moitié pour les accès mémoire.
{|class="wikitable"
|-
! Port 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
|}
Les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon, sont un exemple moins représentatif. Ces microarchitectures utilisent le partage des ports d'émission au maximum. C'était des microarchitectures à triple émission, avec six ports d’émission. Il y avait trois ports d'émission pour les opérations flottantes, et trois ports d'émission pour les micro-opérations entières/mémoire. Il y avait trois ALU entières et trois unités mémoire, chaque ALU partageait un port d'émission avec une unité mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements.
{|class="wikitable"
|-
! Port 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 || ALU entière || FADD || FMUL || FPU (autres opérations)
|-
| Multiplieur || || || || ||
|-
| LOAD/STORE || LOAD/STORE || LOAD/STORE || || ||
|}
Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair.
Par exemple, la microarchitecture ''Golden Cove'' a deux ''barrel shifters''.
{|class="wikitable"
|-
! Port 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 !! Port d'émission n°7 !! Port d'émission n°8 !! Port d'émission n°9 !! Port d'émission n°10
|-
| ALU entière || ALU entière || ALU entière || ALU entière || ALU entière || LOAD || LOAD || LOAD || STORE || STORE
|-
| ''Barrel shifter'' || Multiplieur || Multiplieur || ''Barrel shifter'' || || || || || ||
|-
| Branchements || Diviseur || || Branchements || || || || || ||
|}
==L'évolution historique de la superscalarité==
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. La plupart des CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 sont rapidement montés à de la quadruple émission, avec quelques exceptions. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 ||
|-
! 1995
| 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000
|-
! 1996
| class="f_bleu | 620 || || || || class="f_bleu | 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
===Les processeurs historiques à double émission===
Les premiers CPU à double émission sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Voici le tableau chronologique disant quel CPU utilisait quelle technique.
* Les CPU en jaune utilisaient un ''partitionnement complet'' entre ALU, FPU et unité mémoire.
* Les CPU en vert utilisaient la double émission entière-flottante.
* Les CPU en rouge utilisaient la double émission entière.
* Les CPU sans couleur utilisaient un grand nombre d'unités, avec beaucoup de ports d'émission.
* Les CPU barrés utilisent la triple ou quadruple émission et seront vu dans la section suivante.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| class="f_vert | RS/6000 || || || class="f_rouge | i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| class="f_vert | RSC || class="f_vert | PA 7100 || <s>SuperSPARC</s> || || class="f_jaune | 21064 ||
|-
! 1993
| class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || ||
|-
! 1994
| <s>POWER 2</s> || PA 7200 || || || <s>21164</s> ||
|}
Le PA 7200 est un PA-7100 auquel on aurait rajouté une ALU entière. Le processeur exécutait les multiplications entières dans la FPU, comme son prédécesseur. Les combinaisons autorisées étaient donc INT + INT, INT + MEM, INT + FLOAT/MUL, MEM + FLOAT/MUL.
===Les processeurs historiques à quadruple émission===
C'est la même année , en 1994, que la quadruple émission est arrivée et que les CPU ont commencé à avoir de plus en plus d'unités de calcul. Même les designs restés en double émission avaient ajouté des unités de calcul. Il y avait alors trois stratégies pour la quadruple émission.
La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Les FPUs étaient soit dupliquées, soit scindées en deux en séparant l'additionneur du multiplieur flottant. Le POWER 2 dupliquait ses FPUs à la perfection, alors que l'21264 partitionnait la FPU. Les accès mémoire étaient traités comme des opérations entières, et le processeur pouvait : soit en faire un seul par cycle, soit en émettre deux, sous conditions (le cache devait être double port pour lire/écrire deux données à la fois, comme on le verra dans la suite du chapitre).
Les processeurs MIPS R8000 et R10000 allaient un peu plus loin, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités.
La seconde implémentation dupliquait l'ALU entière et combinait le tout avec un partitionnement INT-FLOAT-MEM. De quoi exécuter en même temps deux opérations entières, une opération flottante, et un accès mémoire. 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).
Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, avec au minimum deux ALU entières. Pour comprendre ce que je veux dire, prenons l'exemple de l'UltraSPARC. Il avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplier flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes. Le processeur pouvait émettre 4 instructions, tant qu'elles atterrissaient dans des unités différentes.
La même méthode peut être utilisée pour la double émission. Le Motorola 88110, sorti en 1991, était un CPU à double émission avec pas moins de 10 unités : deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise.
La morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes. Le tout est résumé dans le tableau ci-dessous, avec :
* en vert les CPU utilisant la première stratégie ;
* en rouge les CPU utilisant la seconde stratégie ;
* en jaune les CPU utilisant la troisième stratégie.
{|class="wikitable"
|+ Processeurs quadruple émission historiques
|-
! Année !! Processeur !! Unités
|-
! 1991
| class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques
|-
|-
! rowspan="2" | 1994
| class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'')
|-
| class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM
|-
! rowspan="3" | 1995
| class="f_jaune | PA 8000 || class="f_jaune | 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD
|-
| class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques
|-
| MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM
|-
! rowspan="2" | 1996
| class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM
|-
| class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL)
|}
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les CPU superscalaire à exécution dans le désordre==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Les unités d'exécution dans le désordre===
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports de ''dispatch''. Mais avec des stations de réservation, il faut le double des ports de ''schedule''. La raison est que les registres sont lus après émission avec une fenê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 de ''schedule'' que de ''dispatch''.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de 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===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Avec plusieurs dizaines d'unités de calcul différentes, ajouter autant 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.
La première solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Le processeur détecte quand il n'y a pas assez de ports pour servir toutes les micro-opérations. Quand ça arrive, l'unité d'émission émet assez de micro-opérations pour exploiter les ports disponibles, mais met en attente les micro-opérations en trop, le temps que les ports se libèrent. Pour cela, le banc de registre est géré par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Une autre solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une dernière solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
468832dnbeukzskxfx060covqm8z1eb
773200
773199
2026-09-25T20:59:20Z
Mewtow
31375
/* Les processeurs historiques à quadruple émission */
773200
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===L'impact des contraintes d’appariement sur la performance===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour la suite du chapitre===
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==Le ''front-end'' des processeurs superscalaires==
Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple.
Intuitivement, charger, décoder et 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 certaines processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. 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
|}
Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs.
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===L'émission parallèle des instructions===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elle sont propagée au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage).
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
==Les processeurs implémentés avec plusieurs pipelines==
Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Vous vous dites que l'implémentation d'un processeur superscalaire n'est pas la même suivant que le CPU utilise l'exécution dans le désordre, ou non. Mais dans les faits, la distinction est plus subtile que ca. Pour l'expliquer, je vais utiliser une classification originale, qui sépare les processeurs superscalaires en trois sous-types. La distinction se fait sur la manière dont la superscalarité est implémentée en matériel.La classification que j'utilise distingue :
* Les processeurs avec plusieurs pipelines, sans exécution dans le désordre.
* Les processeurs superscalaires avec un pipeline dynamique.
* Les processeurs avec des fenêtres d'instruction décentralisées.
La distinction n'est pas qu'une question d'implémentation matérielle. Elle impacte aussi le ratio entre instructions chargées/décodées et instructions émises. Vous vous dites que les deux sont égales, mais ce n'est le cas que sur les processeurs avec N pipelines ! Sur les autres processeurs, il est possible de décoder 4 instructions, mais d'en émettre maximum 6 par cycle ! Et sur les CPU avec des fenêtres d'instruction décentralisées, il y a aussi une différence entre le nombre d'instructions qui sont ''dispatch'' et ''schedule''.
La catégorie la plus simple est celle des '''processeurs avec plusieurs pipelines'''. Ce sont des processeurs superscalaires sans exécution dans le désordre. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction.
Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, d'une FPU, éventuellement d'un second banc de registres, guère plus. Il permet d'exécuter une seconde opération de calcul, pas plus, ce qui est à l'origine de contraintes d'appariemment assez fortes. De plus, il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section.
===La double émission entière-flottante===
L'exemple le plus simple utilise un pipeline pour les opérations flottantes, séparé du pipeline originel. Pour bien expliquer les choses, prenons le processeur illustré ci-dessous. Un processeur a deux ''back-end'' séparés, un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par l'ALU entière, passent éventuellement dans l'unité mémoire, pour ensuite arriver à l'étape de ''writeback''. La FPU et le banc de registre flottant ont eux aussi leur propre ''back-end'' séparé.
[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]]
Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaqaue pipeline, à chaque cycle.
L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine.
Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants.
===La double émission entière===
Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait.
Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
==Les processeurs superscalaires dynamiques==
Les processeurs précédents, avec 2 pipelines, avaient une unité d'émission assez simple. Elle avait deux ports de décodage, et deux ports d'émission. Le nombre de ports de décodage et d'émission étaient identiques. Vous vous dites que c'est naturel, et intuitif, que c'est normal pour un processeur superscalaire. Mais en réalité, la majorité des processeurs superscalaires a plus de ports d'émission que de décodage. Expliquer pourquoi va cependant nous demander de parler de comment la superscalarité exploite les unités de calcul.
Posons-nous cette question : de combien d'unités un processeur superscalaire a besoin ? Les unités en question ne sont nécessairement des unité de calcul, l'unité mémoire et l'unité de branchement comptent aussi. La réponse dépend du nombre de voies. Si le ''front-end'' charge et décode un paquet de N instructions, exécuter ces N instructions simultanément demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section.
La réponse dépend aussi des contraintes d’appariement. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission parfaite devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement, ce qui ferait passer de 6 unités à 12. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, le cout en circuit encore plus prohibitif !
En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée, alors que la double émission parait plus adaptée.
Et c'est là que la différence entre ports d'émission et de décodage devient intéressante. L'idée est d'avoir une unité d'émission avec deux ports de décodage, et 4 port d'émission (un par unité). Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux micro-opérations tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu.
Les tout premiers CPU superscalaires étaient nombreux à utiliser cette technique. Les exemples classiques sont l'HyperSPARC, de l'alpha 21064 et du PowerPC 603. C'étaient des CPU double émission, avec une ALU entière, une unité mémoire et une FPU. Nous parlerons de '''partitionnement INT-FLOAT-MEM'''. Les contraintes d’appariement variaient grandement suivant le processeur.
Le PowerPC 603 implémentait une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur.
Avant de poursuivre, précisons que la FPU regroupe un additionneur flottant et un multiplieur flottant. Et ce fait peut être exploité pour approfondir le partitionnement. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions).
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple, facile à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. La raison est que le partitionnement permet d'émettre des paires, triplets, quadruplets de micro-opérations qui sont rares en pratique.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''.
Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, 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. Et enfin, il faut rajouter un port d'émission pour pouvoir émettre une micro-opération entière en plus.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle.
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
Reste à connecter les unités de calcul à l'unité d'émission (indirectement, car il y a des étages de pipeline entre les deux). Une implémentation basique utilisee un port d'émission par unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La présence d'un circuit multiplieur et d'un ''barrel shifter'' pose des problèmes pour le partitionnement. Il est possible d'avoir un port d'émission dédié au multiplieur, et un autre au ''barrel shifter'', qui sont séparés de ceux pour l'ALU entière. Il est alors possible d'émettre une opération entière, une multiplication et un décalage, simultanément. Nous parlerons de '''partitionnement entier''' pour désigner cette possibilité.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
Le partitionnement entier est cependant très rare, car le gain en performance serait trop faible au regard des couts. Quelques processeurs historiques l'utilisaient, mais ce n'est plus le cas de nos jours. La raison est que les multiplications et les décalages sont assez rares dans le code, ce qui fait que le port d'émission associés sont sous-utilisés. Et un port d'émission a un cout en circuit assez important. 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.
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul. L'exemple classique est justement lié aux circuits multiplieur et ''barrel shifters''. L'idée est que le multiplieur et le ''barrel shifter'' sont mis sur le même port d'émission qu'une ALU entière.
Prenons par exemple le processeur à double émission entière illustré ci-contre. Un port d'émission est relié à une ALU entière seule, et l'autre port d'émission alimente la seconde ALU entière et le multiplieur. La conséquence est que le processeur peut émettre deux opérations entières, dont une multiplication. Si on avait utilisé un port d'émission dédié au multiplieur, il aurait été possible d'émettre deux opérations entières et une multiplication. On perd donc une voie, mais on gagne en flexibilité et on économise des circuits.
[[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]]
En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais le partage des ports d'émission peut appliquer à n'importe quelle unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. 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.
La solution est de partager des ports d'émission, pour passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple relie une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites. Le CPU peut émettre 6 opérations entières simultanément, grâce à 6 ALU entières. Les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires mélangent les trois techniques===
Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autre unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents.
* Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU.
* La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants.
* La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables.
* Le '''partage des ports d'émission''' réduit le cout en circuit du CPU, pour des pertes de performances mineures si bien fait.
De nos jours, le partitionnement est utilisé en complément de la duplication des ALU entières, de la duplication des autres unités, et du partage des ports d'émission. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. Le partage des ports d'émission a lui aussi mis un peu de temps pour s'imposer.
Les CPU superscalaires tendent à mettre le paquet niveau opérations arithmétiques, mais pas forcément au point de remplir la totalité de la largeur du processeur. La règle est que pour N voies, le processeur dispose d'au moins N/2 ALU entières. Et N/2 est une borne basse, la plupart des CPU x86 ont environ tournent plutôt autour des 2/3, voire des 3/4. Attention, cela ne signifie pas que le processeur émet systématiquement N/2 opération entières, il peut par exemple remplacer des opérations entières par des opérations flottantes.
En général, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. La conséquence est que les processeurs ont commencé à dupliquer leurs unités mémoire dès qu'ils ont atteint la quadruple émission, pour les CPU grand public. Par exemple, la microarchitecture ''Golden Cove'' d'Intel dispose de cinq unités de calcul, trois unités mémoire (deux spécialisées dans les écritures, trois dans les lectures). La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire. Notez que le ratio est équilibré entre opérations entières et mémoire, avec soit égalité, soit un avantage pour les ALU entières.
Une autre régularité est que la largeur totale du processeur peut être occupée par des opérations entières/mémoire. Pour le dire autrement, sur un processeur quadruple émission, il pourra émettre un mix de 4 instructions entière/mémoire, avec zéro opération flottante. Idem avec des processeurs triples ou pentuple émission, voire plus. Si le processeur émet une opération flottante, elle remplace une opération entière.
Prenons l'exemple des 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. C'était des processeurs quadruple émission, ce qui fait que la moitié de la largeur du processeur sert pour des opérations entières, et l'autre moitié pour les accès mémoire.
{|class="wikitable"
|-
! Port 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
|}
Les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon, sont un exemple moins représentatif. Ces microarchitectures utilisent le partage des ports d'émission au maximum. C'était des microarchitectures à triple émission, avec six ports d’émission. Il y avait trois ports d'émission pour les opérations flottantes, et trois ports d'émission pour les micro-opérations entières/mémoire. Il y avait trois ALU entières et trois unités mémoire, chaque ALU partageait un port d'émission avec une unité mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements.
{|class="wikitable"
|-
! Port 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 || ALU entière || FADD || FMUL || FPU (autres opérations)
|-
| Multiplieur || || || || ||
|-
| LOAD/STORE || LOAD/STORE || LOAD/STORE || || ||
|}
Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair.
Par exemple, la microarchitecture ''Golden Cove'' a deux ''barrel shifters''.
{|class="wikitable"
|-
! Port 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 !! Port d'émission n°7 !! Port d'émission n°8 !! Port d'émission n°9 !! Port d'émission n°10
|-
| ALU entière || ALU entière || ALU entière || ALU entière || ALU entière || LOAD || LOAD || LOAD || STORE || STORE
|-
| ''Barrel shifter'' || Multiplieur || Multiplieur || ''Barrel shifter'' || || || || || ||
|-
| Branchements || Diviseur || || Branchements || || || || || ||
|}
==L'évolution historique de la superscalarité==
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. La plupart des CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 sont rapidement montés à de la quadruple émission, avec quelques exceptions. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 ||
|-
! 1995
| 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000
|-
! 1996
| class="f_bleu | 620 || || || || class="f_bleu | 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
===Les processeurs historiques à double émission===
Les premiers CPU à double émission sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Voici le tableau chronologique disant quel CPU utilisait quelle technique.
* Les CPU en jaune utilisaient un ''partitionnement complet'' entre ALU, FPU et unité mémoire.
* Les CPU en vert utilisaient la double émission entière-flottante.
* Les CPU en rouge utilisaient la double émission entière.
* Les CPU sans couleur utilisaient un grand nombre d'unités, avec beaucoup de ports d'émission.
* Les CPU barrés utilisent la triple ou quadruple émission et seront vu dans la section suivante.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| class="f_vert | RS/6000 || || || class="f_rouge | i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| class="f_vert | RSC || class="f_vert | PA 7100 || <s>SuperSPARC</s> || || class="f_jaune | 21064 ||
|-
! 1993
| class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || ||
|-
! 1994
| <s>POWER 2</s> || PA 7200 || || || <s>21164</s> ||
|}
Le PA 7200 est un PA-7100 auquel on aurait rajouté une ALU entière. Le processeur exécutait les multiplications entières dans la FPU, comme son prédécesseur. Les combinaisons autorisées étaient donc INT + INT, INT + MEM, INT + FLOAT/MUL, MEM + FLOAT/MUL.
===Les processeurs historiques à quadruple émission===
C'est la même année , en 1994, que la quadruple émission est arrivée et que les CPU ont commencé à avoir de plus en plus d'unités de calcul. Même les designs restés en double émission avaient ajouté des unités de calcul. Il y avait alors trois stratégies pour la quadruple émission.
La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Les FPUs étaient soit dupliquées, soit scindées en deux en séparant l'additionneur du multiplieur flottant. Le POWER 2 dupliquait ses FPUs à la perfection, alors que l'21264 partitionnait la FPU. Les accès mémoire étaient traités comme des opérations entières, et le processeur pouvait : soit en faire un seul par cycle, soit en émettre deux, sous conditions (le cache devait être double port pour lire/écrire deux données à la fois, comme on le verra dans la suite du chapitre).
Les processeurs MIPS R8000 et R10000 allaient un peu plus loin, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités.
La seconde implémentation dupliquait l'ALU entière et combinait le tout avec un partitionnement INT-FLOAT-MEM. De quoi exécuter en même temps deux opérations entières, une opération flottante, et un accès mémoire. 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).
Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, avec au minimum deux ALU entières. Pour comprendre ce que je veux dire, prenons l'exemple de l'UltraSPARC. Il avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplier flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes. Le processeur pouvait émettre 4 instructions, tant qu'elles atterrissaient dans des unités différentes.
La même méthode peut être utilisée pour la double émission. Le Motorola 88110, sorti en 1991, était un CPU à double émission avec pas moins de 10 unités : deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise.
La morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes. Le tout est résumé dans le tableau ci-dessous, avec :
* en vert les CPU utilisant la première stratégie ;
* en rouge les CPU utilisant la seconde stratégie ;
* en jaune les CPU utilisant la troisième stratégie.
{|class="wikitable"
|+ Processeurs quadruple émission historiques
|-
! Année !! Processeur !! Unités
|-
! 1991
| class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques
|-
|-
! rowspan="2" | 1994
| class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'')
|-
| class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM
|-
! rowspan="3" | 1995
| class="f_jaune | PA 8000 || class="f_jaune | 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD
|-
| class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques
|-
| MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM
|-
! rowspan="2" | 1996
| class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM
|-
| class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL)
|}
Le SuperSPARC était un CPU triple émission, avec deux ALU entières, une FPU et une unité mémoire. Il avait de nombreuses contraintes d’appariement. Par exemple, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Une bonne partie de ces contraintes était lié au banc de registre, qui n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions. Il faut noter que la seconde ALU a été abandonnée lors du passage au CPU HyperSPARC, qui est de plus repassé en double émission, ce qui est une évolution à rebours de l'évolution des autres CPUs.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les CPU superscalaire à exécution dans le désordre==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Les unités d'exécution dans le désordre===
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports de ''dispatch''. Mais avec des stations de réservation, il faut le double des ports de ''schedule''. La raison est que les registres sont lus après émission avec une fenê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 de ''schedule'' que de ''dispatch''.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de 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===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Avec plusieurs dizaines d'unités de calcul différentes, ajouter autant 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.
La première solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Le processeur détecte quand il n'y a pas assez de ports pour servir toutes les micro-opérations. Quand ça arrive, l'unité d'émission émet assez de micro-opérations pour exploiter les ports disponibles, mais met en attente les micro-opérations en trop, le temps que les ports se libèrent. Pour cela, le banc de registre est géré par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Une autre solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une dernière solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
fe6p4oyt62jza2nd8io84fk74pvqcdp
773201
773200
2026-09-25T20:59:46Z
Mewtow
31375
/* Les processeurs historiques à double émission */
773201
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===L'impact des contraintes d’appariement sur la performance===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour la suite du chapitre===
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==Le ''front-end'' des processeurs superscalaires==
Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple.
Intuitivement, charger, décoder et 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 certaines processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. 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
|}
Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs.
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===L'émission parallèle des instructions===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elle sont propagée au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage).
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
==Les processeurs implémentés avec plusieurs pipelines==
Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Vous vous dites que l'implémentation d'un processeur superscalaire n'est pas la même suivant que le CPU utilise l'exécution dans le désordre, ou non. Mais dans les faits, la distinction est plus subtile que ca. Pour l'expliquer, je vais utiliser une classification originale, qui sépare les processeurs superscalaires en trois sous-types. La distinction se fait sur la manière dont la superscalarité est implémentée en matériel.La classification que j'utilise distingue :
* Les processeurs avec plusieurs pipelines, sans exécution dans le désordre.
* Les processeurs superscalaires avec un pipeline dynamique.
* Les processeurs avec des fenêtres d'instruction décentralisées.
La distinction n'est pas qu'une question d'implémentation matérielle. Elle impacte aussi le ratio entre instructions chargées/décodées et instructions émises. Vous vous dites que les deux sont égales, mais ce n'est le cas que sur les processeurs avec N pipelines ! Sur les autres processeurs, il est possible de décoder 4 instructions, mais d'en émettre maximum 6 par cycle ! Et sur les CPU avec des fenêtres d'instruction décentralisées, il y a aussi une différence entre le nombre d'instructions qui sont ''dispatch'' et ''schedule''.
La catégorie la plus simple est celle des '''processeurs avec plusieurs pipelines'''. Ce sont des processeurs superscalaires sans exécution dans le désordre. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction.
Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, d'une FPU, éventuellement d'un second banc de registres, guère plus. Il permet d'exécuter une seconde opération de calcul, pas plus, ce qui est à l'origine de contraintes d'appariemment assez fortes. De plus, il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section.
===La double émission entière-flottante===
L'exemple le plus simple utilise un pipeline pour les opérations flottantes, séparé du pipeline originel. Pour bien expliquer les choses, prenons le processeur illustré ci-dessous. Un processeur a deux ''back-end'' séparés, un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par l'ALU entière, passent éventuellement dans l'unité mémoire, pour ensuite arriver à l'étape de ''writeback''. La FPU et le banc de registre flottant ont eux aussi leur propre ''back-end'' séparé.
[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]]
Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaqaue pipeline, à chaque cycle.
L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine.
Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants.
===La double émission entière===
Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait.
Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
==Les processeurs superscalaires dynamiques==
Les processeurs précédents, avec 2 pipelines, avaient une unité d'émission assez simple. Elle avait deux ports de décodage, et deux ports d'émission. Le nombre de ports de décodage et d'émission étaient identiques. Vous vous dites que c'est naturel, et intuitif, que c'est normal pour un processeur superscalaire. Mais en réalité, la majorité des processeurs superscalaires a plus de ports d'émission que de décodage. Expliquer pourquoi va cependant nous demander de parler de comment la superscalarité exploite les unités de calcul.
Posons-nous cette question : de combien d'unités un processeur superscalaire a besoin ? Les unités en question ne sont nécessairement des unité de calcul, l'unité mémoire et l'unité de branchement comptent aussi. La réponse dépend du nombre de voies. Si le ''front-end'' charge et décode un paquet de N instructions, exécuter ces N instructions simultanément demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section.
La réponse dépend aussi des contraintes d’appariement. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission parfaite devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement, ce qui ferait passer de 6 unités à 12. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, le cout en circuit encore plus prohibitif !
En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée, alors que la double émission parait plus adaptée.
Et c'est là que la différence entre ports d'émission et de décodage devient intéressante. L'idée est d'avoir une unité d'émission avec deux ports de décodage, et 4 port d'émission (un par unité). Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux micro-opérations tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu.
Les tout premiers CPU superscalaires étaient nombreux à utiliser cette technique. Les exemples classiques sont l'HyperSPARC, de l'alpha 21064 et du PowerPC 603. C'étaient des CPU double émission, avec une ALU entière, une unité mémoire et une FPU. Nous parlerons de '''partitionnement INT-FLOAT-MEM'''. Les contraintes d’appariement variaient grandement suivant le processeur.
Le PowerPC 603 implémentait une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur.
Avant de poursuivre, précisons que la FPU regroupe un additionneur flottant et un multiplieur flottant. Et ce fait peut être exploité pour approfondir le partitionnement. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions).
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple, facile à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. La raison est que le partitionnement permet d'émettre des paires, triplets, quadruplets de micro-opérations qui sont rares en pratique.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''.
Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, 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. Et enfin, il faut rajouter un port d'émission pour pouvoir émettre une micro-opération entière en plus.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle.
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
Reste à connecter les unités de calcul à l'unité d'émission (indirectement, car il y a des étages de pipeline entre les deux). Une implémentation basique utilisee un port d'émission par unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La présence d'un circuit multiplieur et d'un ''barrel shifter'' pose des problèmes pour le partitionnement. Il est possible d'avoir un port d'émission dédié au multiplieur, et un autre au ''barrel shifter'', qui sont séparés de ceux pour l'ALU entière. Il est alors possible d'émettre une opération entière, une multiplication et un décalage, simultanément. Nous parlerons de '''partitionnement entier''' pour désigner cette possibilité.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
Le partitionnement entier est cependant très rare, car le gain en performance serait trop faible au regard des couts. Quelques processeurs historiques l'utilisaient, mais ce n'est plus le cas de nos jours. La raison est que les multiplications et les décalages sont assez rares dans le code, ce qui fait que le port d'émission associés sont sous-utilisés. Et un port d'émission a un cout en circuit assez important. 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.
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul. L'exemple classique est justement lié aux circuits multiplieur et ''barrel shifters''. L'idée est que le multiplieur et le ''barrel shifter'' sont mis sur le même port d'émission qu'une ALU entière.
Prenons par exemple le processeur à double émission entière illustré ci-contre. Un port d'émission est relié à une ALU entière seule, et l'autre port d'émission alimente la seconde ALU entière et le multiplieur. La conséquence est que le processeur peut émettre deux opérations entières, dont une multiplication. Si on avait utilisé un port d'émission dédié au multiplieur, il aurait été possible d'émettre deux opérations entières et une multiplication. On perd donc une voie, mais on gagne en flexibilité et on économise des circuits.
[[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]]
En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais le partage des ports d'émission peut appliquer à n'importe quelle unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. 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.
La solution est de partager des ports d'émission, pour passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple relie une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites. Le CPU peut émettre 6 opérations entières simultanément, grâce à 6 ALU entières. Les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires mélangent les trois techniques===
Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autre unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents.
* Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU.
* La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants.
* La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables.
* Le '''partage des ports d'émission''' réduit le cout en circuit du CPU, pour des pertes de performances mineures si bien fait.
De nos jours, le partitionnement est utilisé en complément de la duplication des ALU entières, de la duplication des autres unités, et du partage des ports d'émission. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. Le partage des ports d'émission a lui aussi mis un peu de temps pour s'imposer.
Les CPU superscalaires tendent à mettre le paquet niveau opérations arithmétiques, mais pas forcément au point de remplir la totalité de la largeur du processeur. La règle est que pour N voies, le processeur dispose d'au moins N/2 ALU entières. Et N/2 est une borne basse, la plupart des CPU x86 ont environ tournent plutôt autour des 2/3, voire des 3/4. Attention, cela ne signifie pas que le processeur émet systématiquement N/2 opération entières, il peut par exemple remplacer des opérations entières par des opérations flottantes.
En général, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. La conséquence est que les processeurs ont commencé à dupliquer leurs unités mémoire dès qu'ils ont atteint la quadruple émission, pour les CPU grand public. Par exemple, la microarchitecture ''Golden Cove'' d'Intel dispose de cinq unités de calcul, trois unités mémoire (deux spécialisées dans les écritures, trois dans les lectures). La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire. Notez que le ratio est équilibré entre opérations entières et mémoire, avec soit égalité, soit un avantage pour les ALU entières.
Une autre régularité est que la largeur totale du processeur peut être occupée par des opérations entières/mémoire. Pour le dire autrement, sur un processeur quadruple émission, il pourra émettre un mix de 4 instructions entière/mémoire, avec zéro opération flottante. Idem avec des processeurs triples ou pentuple émission, voire plus. Si le processeur émet une opération flottante, elle remplace une opération entière.
Prenons l'exemple des 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. C'était des processeurs quadruple émission, ce qui fait que la moitié de la largeur du processeur sert pour des opérations entières, et l'autre moitié pour les accès mémoire.
{|class="wikitable"
|-
! Port 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
|}
Les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon, sont un exemple moins représentatif. Ces microarchitectures utilisent le partage des ports d'émission au maximum. C'était des microarchitectures à triple émission, avec six ports d’émission. Il y avait trois ports d'émission pour les opérations flottantes, et trois ports d'émission pour les micro-opérations entières/mémoire. Il y avait trois ALU entières et trois unités mémoire, chaque ALU partageait un port d'émission avec une unité mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements.
{|class="wikitable"
|-
! Port 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 || ALU entière || FADD || FMUL || FPU (autres opérations)
|-
| Multiplieur || || || || ||
|-
| LOAD/STORE || LOAD/STORE || LOAD/STORE || || ||
|}
Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair.
Par exemple, la microarchitecture ''Golden Cove'' a deux ''barrel shifters''.
{|class="wikitable"
|-
! Port 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 !! Port d'émission n°7 !! Port d'émission n°8 !! Port d'émission n°9 !! Port d'émission n°10
|-
| ALU entière || ALU entière || ALU entière || ALU entière || ALU entière || LOAD || LOAD || LOAD || STORE || STORE
|-
| ''Barrel shifter'' || Multiplieur || Multiplieur || ''Barrel shifter'' || || || || || ||
|-
| Branchements || Diviseur || || Branchements || || || || || ||
|}
==L'évolution historique de la superscalarité==
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. La plupart des CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 sont rapidement montés à de la quadruple émission, avec quelques exceptions. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 ||
|-
! 1995
| 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000
|-
! 1996
| class="f_bleu | 620 || || || || class="f_bleu | 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
===Les processeurs historiques à double émission===
Les premiers CPU à double émission sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Voici le tableau chronologique disant quel CPU utilisait quelle technique.
* Les CPU en vert utilisaient la double émission entière-flottante.
* Les CPU en rouge utilisaient la double émission entière.
* Les CPU en jaune utilisaient un ''partitionnement complet'' entre ALU, FPU et unité mémoire.
* Les CPU sans couleur utilisaient un grand nombre d'unités, avec beaucoup de ports d'émission.
* Les CPU barrés utilisent la triple ou quadruple émission et seront vu dans la section suivante.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| class="f_vert | RS/6000 || || || class="f_rouge | i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| class="f_vert | RSC || class="f_vert | PA 7100 || <s>SuperSPARC</s> || || class="f_jaune | 21064 ||
|-
! 1993
| class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || ||
|-
! 1994
| <s>POWER 2</s> || PA 7200 || || || <s>21164</s> ||
|}
Le PA 7200 est un PA-7100 auquel on aurait rajouté une ALU entière. Le processeur exécutait les multiplications entières dans la FPU, comme son prédécesseur. Les combinaisons autorisées étaient donc INT + INT, INT + MEM, INT + FLOAT/MUL, MEM + FLOAT/MUL.
===Les processeurs historiques à quadruple émission===
C'est la même année , en 1994, que la quadruple émission est arrivée et que les CPU ont commencé à avoir de plus en plus d'unités de calcul. Même les designs restés en double émission avaient ajouté des unités de calcul. Il y avait alors trois stratégies pour la quadruple émission.
La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Les FPUs étaient soit dupliquées, soit scindées en deux en séparant l'additionneur du multiplieur flottant. Le POWER 2 dupliquait ses FPUs à la perfection, alors que l'21264 partitionnait la FPU. Les accès mémoire étaient traités comme des opérations entières, et le processeur pouvait : soit en faire un seul par cycle, soit en émettre deux, sous conditions (le cache devait être double port pour lire/écrire deux données à la fois, comme on le verra dans la suite du chapitre).
Les processeurs MIPS R8000 et R10000 allaient un peu plus loin, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités.
La seconde implémentation dupliquait l'ALU entière et combinait le tout avec un partitionnement INT-FLOAT-MEM. De quoi exécuter en même temps deux opérations entières, une opération flottante, et un accès mémoire. 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).
Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, avec au minimum deux ALU entières. Pour comprendre ce que je veux dire, prenons l'exemple de l'UltraSPARC. Il avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplier flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes. Le processeur pouvait émettre 4 instructions, tant qu'elles atterrissaient dans des unités différentes.
La même méthode peut être utilisée pour la double émission. Le Motorola 88110, sorti en 1991, était un CPU à double émission avec pas moins de 10 unités : deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise.
La morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes. Le tout est résumé dans le tableau ci-dessous, avec :
* en vert les CPU utilisant la première stratégie ;
* en rouge les CPU utilisant la seconde stratégie ;
* en jaune les CPU utilisant la troisième stratégie.
{|class="wikitable"
|+ Processeurs quadruple émission historiques
|-
! Année !! Processeur !! Unités
|-
! 1991
| class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques
|-
|-
! rowspan="2" | 1994
| class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'')
|-
| class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM
|-
! rowspan="3" | 1995
| class="f_jaune | PA 8000 || class="f_jaune | 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD
|-
| class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques
|-
| MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM
|-
! rowspan="2" | 1996
| class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM
|-
| class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL)
|}
Le SuperSPARC était un CPU triple émission, avec deux ALU entières, une FPU et une unité mémoire. Il avait de nombreuses contraintes d’appariement. Par exemple, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Une bonne partie de ces contraintes était lié au banc de registre, qui n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions. Il faut noter que la seconde ALU a été abandonnée lors du passage au CPU HyperSPARC, qui est de plus repassé en double émission, ce qui est une évolution à rebours de l'évolution des autres CPUs.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les CPU superscalaire à exécution dans le désordre==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Les unités d'exécution dans le désordre===
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports de ''dispatch''. Mais avec des stations de réservation, il faut le double des ports de ''schedule''. La raison est que les registres sont lus après émission avec une fenê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 de ''schedule'' que de ''dispatch''.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de 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===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Avec plusieurs dizaines d'unités de calcul différentes, ajouter autant 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.
La première solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Le processeur détecte quand il n'y a pas assez de ports pour servir toutes les micro-opérations. Quand ça arrive, l'unité d'émission émet assez de micro-opérations pour exploiter les ports disponibles, mais met en attente les micro-opérations en trop, le temps que les ports se libèrent. Pour cela, le banc de registre est géré par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Une autre solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une dernière solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
ftx4ow6xvwrkxxf9p7nu0dovdhm22bg
773202
773201
2026-09-25T21:03:26Z
Mewtow
31375
/* Les processeurs historiques à quadruple émission */
773202
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===L'impact des contraintes d’appariement sur la performance===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour la suite du chapitre===
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==Le ''front-end'' des processeurs superscalaires==
Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple.
Intuitivement, charger, décoder et 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 certaines processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. 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
|}
Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs.
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===L'émission parallèle des instructions===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elle sont propagée au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage).
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
==Les processeurs implémentés avec plusieurs pipelines==
Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Vous vous dites que l'implémentation d'un processeur superscalaire n'est pas la même suivant que le CPU utilise l'exécution dans le désordre, ou non. Mais dans les faits, la distinction est plus subtile que ca. Pour l'expliquer, je vais utiliser une classification originale, qui sépare les processeurs superscalaires en trois sous-types. La distinction se fait sur la manière dont la superscalarité est implémentée en matériel.La classification que j'utilise distingue :
* Les processeurs avec plusieurs pipelines, sans exécution dans le désordre.
* Les processeurs superscalaires avec un pipeline dynamique.
* Les processeurs avec des fenêtres d'instruction décentralisées.
La distinction n'est pas qu'une question d'implémentation matérielle. Elle impacte aussi le ratio entre instructions chargées/décodées et instructions émises. Vous vous dites que les deux sont égales, mais ce n'est le cas que sur les processeurs avec N pipelines ! Sur les autres processeurs, il est possible de décoder 4 instructions, mais d'en émettre maximum 6 par cycle ! Et sur les CPU avec des fenêtres d'instruction décentralisées, il y a aussi une différence entre le nombre d'instructions qui sont ''dispatch'' et ''schedule''.
La catégorie la plus simple est celle des '''processeurs avec plusieurs pipelines'''. Ce sont des processeurs superscalaires sans exécution dans le désordre. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction.
Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, d'une FPU, éventuellement d'un second banc de registres, guère plus. Il permet d'exécuter une seconde opération de calcul, pas plus, ce qui est à l'origine de contraintes d'appariemment assez fortes. De plus, il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section.
===La double émission entière-flottante===
L'exemple le plus simple utilise un pipeline pour les opérations flottantes, séparé du pipeline originel. Pour bien expliquer les choses, prenons le processeur illustré ci-dessous. Un processeur a deux ''back-end'' séparés, un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par l'ALU entière, passent éventuellement dans l'unité mémoire, pour ensuite arriver à l'étape de ''writeback''. La FPU et le banc de registre flottant ont eux aussi leur propre ''back-end'' séparé.
[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]]
Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaqaue pipeline, à chaque cycle.
L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine.
Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants.
===La double émission entière===
Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait.
Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
==Les processeurs superscalaires dynamiques==
Les processeurs précédents, avec 2 pipelines, avaient une unité d'émission assez simple. Elle avait deux ports de décodage, et deux ports d'émission. Le nombre de ports de décodage et d'émission étaient identiques. Vous vous dites que c'est naturel, et intuitif, que c'est normal pour un processeur superscalaire. Mais en réalité, la majorité des processeurs superscalaires a plus de ports d'émission que de décodage. Expliquer pourquoi va cependant nous demander de parler de comment la superscalarité exploite les unités de calcul.
Posons-nous cette question : de combien d'unités un processeur superscalaire a besoin ? Les unités en question ne sont nécessairement des unité de calcul, l'unité mémoire et l'unité de branchement comptent aussi. La réponse dépend du nombre de voies. Si le ''front-end'' charge et décode un paquet de N instructions, exécuter ces N instructions simultanément demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section.
La réponse dépend aussi des contraintes d’appariement. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission parfaite devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement, ce qui ferait passer de 6 unités à 12. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, le cout en circuit encore plus prohibitif !
En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée, alors que la double émission parait plus adaptée.
Et c'est là que la différence entre ports d'émission et de décodage devient intéressante. L'idée est d'avoir une unité d'émission avec deux ports de décodage, et 4 port d'émission (un par unité). Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux micro-opérations tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu.
Les tout premiers CPU superscalaires étaient nombreux à utiliser cette technique. Les exemples classiques sont l'HyperSPARC, de l'alpha 21064 et du PowerPC 603. C'étaient des CPU double émission, avec une ALU entière, une unité mémoire et une FPU. Nous parlerons de '''partitionnement INT-FLOAT-MEM'''. Les contraintes d’appariement variaient grandement suivant le processeur.
Le PowerPC 603 implémentait une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur.
Avant de poursuivre, précisons que la FPU regroupe un additionneur flottant et un multiplieur flottant. Et ce fait peut être exploité pour approfondir le partitionnement. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions).
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple, facile à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. La raison est que le partitionnement permet d'émettre des paires, triplets, quadruplets de micro-opérations qui sont rares en pratique.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''.
Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, 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. Et enfin, il faut rajouter un port d'émission pour pouvoir émettre une micro-opération entière en plus.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle.
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
Reste à connecter les unités de calcul à l'unité d'émission (indirectement, car il y a des étages de pipeline entre les deux). Une implémentation basique utilisee un port d'émission par unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La présence d'un circuit multiplieur et d'un ''barrel shifter'' pose des problèmes pour le partitionnement. Il est possible d'avoir un port d'émission dédié au multiplieur, et un autre au ''barrel shifter'', qui sont séparés de ceux pour l'ALU entière. Il est alors possible d'émettre une opération entière, une multiplication et un décalage, simultanément. Nous parlerons de '''partitionnement entier''' pour désigner cette possibilité.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
Le partitionnement entier est cependant très rare, car le gain en performance serait trop faible au regard des couts. Quelques processeurs historiques l'utilisaient, mais ce n'est plus le cas de nos jours. La raison est que les multiplications et les décalages sont assez rares dans le code, ce qui fait que le port d'émission associés sont sous-utilisés. Et un port d'émission a un cout en circuit assez important. 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.
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul. L'exemple classique est justement lié aux circuits multiplieur et ''barrel shifters''. L'idée est que le multiplieur et le ''barrel shifter'' sont mis sur le même port d'émission qu'une ALU entière.
Prenons par exemple le processeur à double émission entière illustré ci-contre. Un port d'émission est relié à une ALU entière seule, et l'autre port d'émission alimente la seconde ALU entière et le multiplieur. La conséquence est que le processeur peut émettre deux opérations entières, dont une multiplication. Si on avait utilisé un port d'émission dédié au multiplieur, il aurait été possible d'émettre deux opérations entières et une multiplication. On perd donc une voie, mais on gagne en flexibilité et on économise des circuits.
[[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]]
En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais le partage des ports d'émission peut appliquer à n'importe quelle unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. 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.
La solution est de partager des ports d'émission, pour passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple relie une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites. Le CPU peut émettre 6 opérations entières simultanément, grâce à 6 ALU entières. Les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires mélangent les trois techniques===
Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autre unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents.
* Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU.
* La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants.
* La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables.
* Le '''partage des ports d'émission''' réduit le cout en circuit du CPU, pour des pertes de performances mineures si bien fait.
De nos jours, le partitionnement est utilisé en complément de la duplication des ALU entières, de la duplication des autres unités, et du partage des ports d'émission. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. Le partage des ports d'émission a lui aussi mis un peu de temps pour s'imposer.
Les CPU superscalaires tendent à mettre le paquet niveau opérations arithmétiques, mais pas forcément au point de remplir la totalité de la largeur du processeur. La règle est que pour N voies, le processeur dispose d'au moins N/2 ALU entières. Et N/2 est une borne basse, la plupart des CPU x86 ont environ tournent plutôt autour des 2/3, voire des 3/4. Attention, cela ne signifie pas que le processeur émet systématiquement N/2 opération entières, il peut par exemple remplacer des opérations entières par des opérations flottantes.
En général, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. La conséquence est que les processeurs ont commencé à dupliquer leurs unités mémoire dès qu'ils ont atteint la quadruple émission, pour les CPU grand public. Par exemple, la microarchitecture ''Golden Cove'' d'Intel dispose de cinq unités de calcul, trois unités mémoire (deux spécialisées dans les écritures, trois dans les lectures). La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire. Notez que le ratio est équilibré entre opérations entières et mémoire, avec soit égalité, soit un avantage pour les ALU entières.
Une autre régularité est que la largeur totale du processeur peut être occupée par des opérations entières/mémoire. Pour le dire autrement, sur un processeur quadruple émission, il pourra émettre un mix de 4 instructions entière/mémoire, avec zéro opération flottante. Idem avec des processeurs triples ou pentuple émission, voire plus. Si le processeur émet une opération flottante, elle remplace une opération entière.
Prenons l'exemple des 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. C'était des processeurs quadruple émission, ce qui fait que la moitié de la largeur du processeur sert pour des opérations entières, et l'autre moitié pour les accès mémoire.
{|class="wikitable"
|-
! Port 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
|}
Les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon, sont un exemple moins représentatif. Ces microarchitectures utilisent le partage des ports d'émission au maximum. C'était des microarchitectures à triple émission, avec six ports d’émission. Il y avait trois ports d'émission pour les opérations flottantes, et trois ports d'émission pour les micro-opérations entières/mémoire. Il y avait trois ALU entières et trois unités mémoire, chaque ALU partageait un port d'émission avec une unité mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements.
{|class="wikitable"
|-
! Port 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 || ALU entière || FADD || FMUL || FPU (autres opérations)
|-
| Multiplieur || || || || ||
|-
| LOAD/STORE || LOAD/STORE || LOAD/STORE || || ||
|}
Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair.
Par exemple, la microarchitecture ''Golden Cove'' a deux ''barrel shifters''.
{|class="wikitable"
|-
! Port 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 !! Port d'émission n°7 !! Port d'émission n°8 !! Port d'émission n°9 !! Port d'émission n°10
|-
| ALU entière || ALU entière || ALU entière || ALU entière || ALU entière || LOAD || LOAD || LOAD || STORE || STORE
|-
| ''Barrel shifter'' || Multiplieur || Multiplieur || ''Barrel shifter'' || || || || || ||
|-
| Branchements || Diviseur || || Branchements || || || || || ||
|}
==L'évolution historique de la superscalarité==
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. La plupart des CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 sont rapidement montés à de la quadruple émission, avec quelques exceptions. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 ||
|-
! 1995
| 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000
|-
! 1996
| class="f_bleu | 620 || || || || class="f_bleu | 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
===Les processeurs historiques à double émission===
Les premiers CPU à double émission sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Voici le tableau chronologique disant quel CPU utilisait quelle technique.
* Les CPU en vert utilisaient la double émission entière-flottante.
* Les CPU en rouge utilisaient la double émission entière.
* Les CPU en jaune utilisaient un ''partitionnement complet'' entre ALU, FPU et unité mémoire.
* Les CPU sans couleur utilisaient un grand nombre d'unités, avec beaucoup de ports d'émission.
* Les CPU barrés utilisent la triple ou quadruple émission et seront vu dans la section suivante.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| class="f_vert | RS/6000 || || || class="f_rouge | i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| class="f_vert | RSC || class="f_vert | PA 7100 || <s>SuperSPARC</s> || || class="f_jaune | 21064 ||
|-
! 1993
| class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || ||
|-
! 1994
| <s>POWER 2</s> || PA 7200 || || || <s>21164</s> ||
|}
Le PA 7200 est un PA-7100 auquel on aurait rajouté une ALU entière. Le processeur exécutait les multiplications entières dans la FPU, comme son prédécesseur. Les combinaisons autorisées étaient donc INT + INT, INT + MEM, INT + FLOAT/MUL, MEM + FLOAT/MUL.
===Les processeurs historiques à quadruple émission===
C'est la même année , en 1994, que la quadruple émission est arrivée et que les CPU ont commencé à avoir de plus en plus d'unités de calcul. Même les designs restés en double émission avaient ajouté des unités de calcul. Il y avait alors trois stratégies pour la quadruple émission.
La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Les FPUs étaient soit dupliquées, soit scindées en deux en séparant l'additionneur du multiplieur flottant. Le POWER 2 dupliquait ses FPUs à la perfection, alors que le 21264 partitionnait la FPU. Les accès mémoire étaient traités comme des opérations entières, et le processeur pouvait : soit en faire un seul par cycle, soit en émettre deux, sous conditions (le cache devait être double port pour lire/écrire deux données à la fois, comme on le verra dans la suite du chapitre).
La seconde implémentation dupliquait l'ALU entière et combinait le tout avec un partitionnement INT-FLOAT-MEM. De quoi exécuter en même temps deux opérations entières, une opération flottante, et un accès mémoire. 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).
Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, avec au minimum deux ALU entières. Pour comprendre ce que je veux dire, prenons l'exemple de l'UltraSPARC. Il avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplier flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes. Le processeur pouvait émettre 4 instructions, tant qu'elles atterrissaient dans des unités différentes.
La même méthode peut être utilisée pour la double émission. Le Motorola 88110, sorti en 1991, était un CPU à double émission avec pas moins de 10 unités : deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise.
Les processeurs MIPS R8000 et R10000 avaient une implémentation plus moderne, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités. Moitié opérations entières, moitié opérations mémoire, avec de quoi remplacer les opérations entières par des opérations flottantes.
La morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes. Le tout est résumé dans le tableau ci-dessous, avec :
* en vert les CPU utilisant la première stratégie ;
* en rouge les CPU utilisant la seconde stratégie ;
* en jaune les CPU utilisant la troisième stratégie.
{|class="wikitable"
|+ Processeurs quadruple émission historiques
|-
! Année !! Processeur !! Unités
|-
! 1991
| class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques
|-
|-
! rowspan="2" | 1994
| class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'')
|-
| class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM
|-
! rowspan="3" | 1995
| class="f_jaune | PA 8000 || class="f_jaune | 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD
|-
| class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques
|-
| MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM
|-
! rowspan="2" | 1996
| class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM
|-
| class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL)
|}
Le SuperSPARC était un CPU triple émission, avec deux ALU entières, une FPU et une unité mémoire. Il avait de nombreuses contraintes d’appariement. Par exemple, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Une bonne partie de ces contraintes était lié au banc de registre, qui n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions. Il faut noter que la seconde ALU a été abandonnée lors du passage au CPU HyperSPARC, qui est de plus repassé en double émission, ce qui est une évolution à rebours de l'évolution des autres CPUs.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les CPU superscalaire à exécution dans le désordre==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Les unités d'exécution dans le désordre===
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports de ''dispatch''. Mais avec des stations de réservation, il faut le double des ports de ''schedule''. La raison est que les registres sont lus après émission avec une fenê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 de ''schedule'' que de ''dispatch''.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de 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===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Avec plusieurs dizaines d'unités de calcul différentes, ajouter autant 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.
La première solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Le processeur détecte quand il n'y a pas assez de ports pour servir toutes les micro-opérations. Quand ça arrive, l'unité d'émission émet assez de micro-opérations pour exploiter les ports disponibles, mais met en attente les micro-opérations en trop, le temps que les ports se libèrent. Pour cela, le banc de registre est géré par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Une autre solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une dernière solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
01odq0oulrdylyrlhvl0atcih5ftpv8
773203
773202
2026-09-25T21:04:25Z
Mewtow
31375
/* Les processeurs historiques à quadruple émission */
773203
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===L'impact des contraintes d’appariement sur la performance===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour la suite du chapitre===
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==Le ''front-end'' des processeurs superscalaires==
Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple.
Intuitivement, charger, décoder et 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 certaines processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. 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
|}
Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs.
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===L'émission parallèle des instructions===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elle sont propagée au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage).
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
==Les processeurs implémentés avec plusieurs pipelines==
Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Vous vous dites que l'implémentation d'un processeur superscalaire n'est pas la même suivant que le CPU utilise l'exécution dans le désordre, ou non. Mais dans les faits, la distinction est plus subtile que ca. Pour l'expliquer, je vais utiliser une classification originale, qui sépare les processeurs superscalaires en trois sous-types. La distinction se fait sur la manière dont la superscalarité est implémentée en matériel.La classification que j'utilise distingue :
* Les processeurs avec plusieurs pipelines, sans exécution dans le désordre.
* Les processeurs superscalaires avec un pipeline dynamique.
* Les processeurs avec des fenêtres d'instruction décentralisées.
La distinction n'est pas qu'une question d'implémentation matérielle. Elle impacte aussi le ratio entre instructions chargées/décodées et instructions émises. Vous vous dites que les deux sont égales, mais ce n'est le cas que sur les processeurs avec N pipelines ! Sur les autres processeurs, il est possible de décoder 4 instructions, mais d'en émettre maximum 6 par cycle ! Et sur les CPU avec des fenêtres d'instruction décentralisées, il y a aussi une différence entre le nombre d'instructions qui sont ''dispatch'' et ''schedule''.
La catégorie la plus simple est celle des '''processeurs avec plusieurs pipelines'''. Ce sont des processeurs superscalaires sans exécution dans le désordre. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction.
Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, d'une FPU, éventuellement d'un second banc de registres, guère plus. Il permet d'exécuter une seconde opération de calcul, pas plus, ce qui est à l'origine de contraintes d'appariemment assez fortes. De plus, il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section.
===La double émission entière-flottante===
L'exemple le plus simple utilise un pipeline pour les opérations flottantes, séparé du pipeline originel. Pour bien expliquer les choses, prenons le processeur illustré ci-dessous. Un processeur a deux ''back-end'' séparés, un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par l'ALU entière, passent éventuellement dans l'unité mémoire, pour ensuite arriver à l'étape de ''writeback''. La FPU et le banc de registre flottant ont eux aussi leur propre ''back-end'' séparé.
[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]]
Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaqaue pipeline, à chaque cycle.
L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine.
Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants.
===La double émission entière===
Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait.
Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
==Les processeurs superscalaires dynamiques==
Les processeurs précédents, avec 2 pipelines, avaient une unité d'émission assez simple. Elle avait deux ports de décodage, et deux ports d'émission. Le nombre de ports de décodage et d'émission étaient identiques. Vous vous dites que c'est naturel, et intuitif, que c'est normal pour un processeur superscalaire. Mais en réalité, la majorité des processeurs superscalaires a plus de ports d'émission que de décodage. Expliquer pourquoi va cependant nous demander de parler de comment la superscalarité exploite les unités de calcul.
Posons-nous cette question : de combien d'unités un processeur superscalaire a besoin ? Les unités en question ne sont nécessairement des unité de calcul, l'unité mémoire et l'unité de branchement comptent aussi. La réponse dépend du nombre de voies. Si le ''front-end'' charge et décode un paquet de N instructions, exécuter ces N instructions simultanément demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section.
La réponse dépend aussi des contraintes d’appariement. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission parfaite devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement, ce qui ferait passer de 6 unités à 12. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, le cout en circuit encore plus prohibitif !
En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée, alors que la double émission parait plus adaptée.
Et c'est là que la différence entre ports d'émission et de décodage devient intéressante. L'idée est d'avoir une unité d'émission avec deux ports de décodage, et 4 port d'émission (un par unité). Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux micro-opérations tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu.
Les tout premiers CPU superscalaires étaient nombreux à utiliser cette technique. Les exemples classiques sont l'HyperSPARC, de l'alpha 21064 et du PowerPC 603. C'étaient des CPU double émission, avec une ALU entière, une unité mémoire et une FPU. Nous parlerons de '''partitionnement INT-FLOAT-MEM'''. Les contraintes d’appariement variaient grandement suivant le processeur.
Le PowerPC 603 implémentait une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur.
Avant de poursuivre, précisons que la FPU regroupe un additionneur flottant et un multiplieur flottant. Et ce fait peut être exploité pour approfondir le partitionnement. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions).
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple, facile à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. La raison est que le partitionnement permet d'émettre des paires, triplets, quadruplets de micro-opérations qui sont rares en pratique.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''.
Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, 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. Et enfin, il faut rajouter un port d'émission pour pouvoir émettre une micro-opération entière en plus.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle.
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
Reste à connecter les unités de calcul à l'unité d'émission (indirectement, car il y a des étages de pipeline entre les deux). Une implémentation basique utilisee un port d'émission par unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La présence d'un circuit multiplieur et d'un ''barrel shifter'' pose des problèmes pour le partitionnement. Il est possible d'avoir un port d'émission dédié au multiplieur, et un autre au ''barrel shifter'', qui sont séparés de ceux pour l'ALU entière. Il est alors possible d'émettre une opération entière, une multiplication et un décalage, simultanément. Nous parlerons de '''partitionnement entier''' pour désigner cette possibilité.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
Le partitionnement entier est cependant très rare, car le gain en performance serait trop faible au regard des couts. Quelques processeurs historiques l'utilisaient, mais ce n'est plus le cas de nos jours. La raison est que les multiplications et les décalages sont assez rares dans le code, ce qui fait que le port d'émission associés sont sous-utilisés. Et un port d'émission a un cout en circuit assez important. 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.
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul. L'exemple classique est justement lié aux circuits multiplieur et ''barrel shifters''. L'idée est que le multiplieur et le ''barrel shifter'' sont mis sur le même port d'émission qu'une ALU entière.
Prenons par exemple le processeur à double émission entière illustré ci-contre. Un port d'émission est relié à une ALU entière seule, et l'autre port d'émission alimente la seconde ALU entière et le multiplieur. La conséquence est que le processeur peut émettre deux opérations entières, dont une multiplication. Si on avait utilisé un port d'émission dédié au multiplieur, il aurait été possible d'émettre deux opérations entières et une multiplication. On perd donc une voie, mais on gagne en flexibilité et on économise des circuits.
[[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]]
En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais le partage des ports d'émission peut appliquer à n'importe quelle unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. 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.
La solution est de partager des ports d'émission, pour passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple relie une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites. Le CPU peut émettre 6 opérations entières simultanément, grâce à 6 ALU entières. Les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires mélangent les trois techniques===
Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autre unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents.
* Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU.
* La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants.
* La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables.
* Le '''partage des ports d'émission''' réduit le cout en circuit du CPU, pour des pertes de performances mineures si bien fait.
De nos jours, le partitionnement est utilisé en complément de la duplication des ALU entières, de la duplication des autres unités, et du partage des ports d'émission. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. Le partage des ports d'émission a lui aussi mis un peu de temps pour s'imposer.
Les CPU superscalaires tendent à mettre le paquet niveau opérations arithmétiques, mais pas forcément au point de remplir la totalité de la largeur du processeur. La règle est que pour N voies, le processeur dispose d'au moins N/2 ALU entières. Et N/2 est une borne basse, la plupart des CPU x86 ont environ tournent plutôt autour des 2/3, voire des 3/4. Attention, cela ne signifie pas que le processeur émet systématiquement N/2 opération entières, il peut par exemple remplacer des opérations entières par des opérations flottantes.
En général, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. La conséquence est que les processeurs ont commencé à dupliquer leurs unités mémoire dès qu'ils ont atteint la quadruple émission, pour les CPU grand public. Par exemple, la microarchitecture ''Golden Cove'' d'Intel dispose de cinq unités de calcul, trois unités mémoire (deux spécialisées dans les écritures, trois dans les lectures). La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire. Notez que le ratio est équilibré entre opérations entières et mémoire, avec soit égalité, soit un avantage pour les ALU entières.
Une autre régularité est que la largeur totale du processeur peut être occupée par des opérations entières/mémoire. Pour le dire autrement, sur un processeur quadruple émission, il pourra émettre un mix de 4 instructions entière/mémoire, avec zéro opération flottante. Idem avec des processeurs triples ou pentuple émission, voire plus. Si le processeur émet une opération flottante, elle remplace une opération entière.
Prenons l'exemple des 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. C'était des processeurs quadruple émission, ce qui fait que la moitié de la largeur du processeur sert pour des opérations entières, et l'autre moitié pour les accès mémoire.
{|class="wikitable"
|-
! Port 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
|}
Les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon, sont un exemple moins représentatif. Ces microarchitectures utilisent le partage des ports d'émission au maximum. C'était des microarchitectures à triple émission, avec six ports d’émission. Il y avait trois ports d'émission pour les opérations flottantes, et trois ports d'émission pour les micro-opérations entières/mémoire. Il y avait trois ALU entières et trois unités mémoire, chaque ALU partageait un port d'émission avec une unité mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements.
{|class="wikitable"
|-
! Port 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 || ALU entière || FADD || FMUL || FPU (autres opérations)
|-
| Multiplieur || || || || ||
|-
| LOAD/STORE || LOAD/STORE || LOAD/STORE || || ||
|}
Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair.
Par exemple, la microarchitecture ''Golden Cove'' a deux ''barrel shifters''.
{|class="wikitable"
|-
! Port 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 !! Port d'émission n°7 !! Port d'émission n°8 !! Port d'émission n°9 !! Port d'émission n°10
|-
| ALU entière || ALU entière || ALU entière || ALU entière || ALU entière || LOAD || LOAD || LOAD || STORE || STORE
|-
| ''Barrel shifter'' || Multiplieur || Multiplieur || ''Barrel shifter'' || || || || || ||
|-
| Branchements || Diviseur || || Branchements || || || || || ||
|}
==L'évolution historique de la superscalarité==
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. La plupart des CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 sont rapidement montés à de la quadruple émission, avec quelques exceptions. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 ||
|-
! 1995
| 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000
|-
! 1996
| class="f_bleu | 620 || || || || class="f_bleu | 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
===Les processeurs historiques à double émission===
Les premiers CPU à double émission sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Voici le tableau chronologique disant quel CPU utilisait quelle technique.
* Les CPU en vert utilisaient la double émission entière-flottante.
* Les CPU en rouge utilisaient la double émission entière.
* Les CPU en jaune utilisaient un ''partitionnement complet'' entre ALU, FPU et unité mémoire.
* Les CPU sans couleur utilisaient un grand nombre d'unités, avec beaucoup de ports d'émission.
* Les CPU barrés utilisent la triple ou quadruple émission et seront vu dans la section suivante.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| class="f_vert | RS/6000 || || || class="f_rouge | i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| class="f_vert | RSC || class="f_vert | PA 7100 || <s>SuperSPARC</s> || || class="f_jaune | 21064 ||
|-
! 1993
| class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || ||
|-
! 1994
| <s>POWER 2</s> || PA 7200 || || || <s>21164</s> ||
|}
Le PA 7200 est un PA-7100 auquel on aurait rajouté une ALU entière. Le processeur exécutait les multiplications entières dans la FPU, comme son prédécesseur. Les combinaisons autorisées étaient donc INT + INT, INT + MEM, INT + FLOAT/MUL, MEM + FLOAT/MUL.
===Les processeurs historiques à quadruple émission===
C'est la même année , en 1994, que la quadruple émission est arrivée et que les CPU ont commencé à avoir de plus en plus d'unités de calcul. Même les designs restés en double émission avaient ajouté des unités de calcul. Il y avait alors trois stratégies pour la quadruple émission.
La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Les FPUs étaient soit dupliquées, soit scindées en deux en séparant l'additionneur du multiplieur flottant. Le POWER 2 dupliquait ses FPUs à la perfection, alors que le 21264 partitionnait la FPU. Les accès mémoire étaient traités comme des opérations entières, et le processeur pouvait : soit en faire un seul par cycle, soit en émettre deux, sous conditions (le cache devait être double port pour lire/écrire deux données à la fois, comme on le verra dans la suite du chapitre).
La seconde implémentation dupliquait l'ALU entière et combinait le tout avec un partitionnement INT-FLOAT-MEM. De quoi exécuter en même temps deux opérations entières, une opération flottante, et un accès mémoire. 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).
Le SuperSPARC était un CPU triple émission, avec deux ALU entières, une FPU et une unité mémoire. Il avait de nombreuses contraintes d’appariement. Par exemple, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Une bonne partie de ces contraintes était lié au banc de registre, qui n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions. Il faut noter que la seconde ALU a été abandonnée lors du passage au CPU HyperSPARC, qui est de plus repassé en double émission, ce qui est une évolution à rebours de l'évolution des autres CPUs.
Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, avec au minimum deux ALU entières. Pour comprendre ce que je veux dire, prenons l'exemple de l'UltraSPARC. Il avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplier flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes. Le processeur pouvait émettre 4 instructions, tant qu'elles atterrissaient dans des unités différentes.
La même méthode peut être utilisée pour la double émission. Le Motorola 88110, sorti en 1991, était un CPU à double émission avec pas moins de 10 unités : deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise.
Les processeurs MIPS R8000 et R10000 avaient une implémentation plus moderne, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités. Moitié opérations entières, moitié opérations mémoire, avec de quoi remplacer les opérations entières par des opérations flottantes.
La morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes. Le tout est résumé dans le tableau ci-dessous, avec :
* en vert les CPU utilisant la première stratégie ;
* en rouge les CPU utilisant la seconde stratégie ;
* en jaune les CPU utilisant la troisième stratégie.
{|class="wikitable"
|+ Processeurs quadruple émission historiques
|-
! Année !! Processeur !! Unités
|-
! 1991
| class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques
|-
|-
! rowspan="2" | 1994
| class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'')
|-
| class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM
|-
! rowspan="3" | 1995
| class="f_jaune | PA 8000 || class="f_jaune | 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD
|-
| class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques
|-
| MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM
|-
! rowspan="2" | 1996
| class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM
|-
| class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL)
|}
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les CPU superscalaire à exécution dans le désordre==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Les unités d'exécution dans le désordre===
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports de ''dispatch''. Mais avec des stations de réservation, il faut le double des ports de ''schedule''. La raison est que les registres sont lus après émission avec une fenê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 de ''schedule'' que de ''dispatch''.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de 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===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Avec plusieurs dizaines d'unités de calcul différentes, ajouter autant 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.
La première solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Le processeur détecte quand il n'y a pas assez de ports pour servir toutes les micro-opérations. Quand ça arrive, l'unité d'émission émet assez de micro-opérations pour exploiter les ports disponibles, mais met en attente les micro-opérations en trop, le temps que les ports se libèrent. Pour cela, le banc de registre est géré par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Une autre solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une dernière solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
7ceofrdx93kl4i0yzp1dq4e363vxse6
773204
773203
2026-09-25T21:05:25Z
Mewtow
31375
/* Les processeurs historiques à quadruple émission */
773204
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===L'impact des contraintes d’appariement sur la performance===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour la suite du chapitre===
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==Le ''front-end'' des processeurs superscalaires==
Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple.
Intuitivement, charger, décoder et 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 certaines processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. 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
|}
Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs.
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===L'émission parallèle des instructions===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elle sont propagée au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage).
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
==Les processeurs implémentés avec plusieurs pipelines==
Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Vous vous dites que l'implémentation d'un processeur superscalaire n'est pas la même suivant que le CPU utilise l'exécution dans le désordre, ou non. Mais dans les faits, la distinction est plus subtile que ca. Pour l'expliquer, je vais utiliser une classification originale, qui sépare les processeurs superscalaires en trois sous-types. La distinction se fait sur la manière dont la superscalarité est implémentée en matériel.La classification que j'utilise distingue :
* Les processeurs avec plusieurs pipelines, sans exécution dans le désordre.
* Les processeurs superscalaires avec un pipeline dynamique.
* Les processeurs avec des fenêtres d'instruction décentralisées.
La distinction n'est pas qu'une question d'implémentation matérielle. Elle impacte aussi le ratio entre instructions chargées/décodées et instructions émises. Vous vous dites que les deux sont égales, mais ce n'est le cas que sur les processeurs avec N pipelines ! Sur les autres processeurs, il est possible de décoder 4 instructions, mais d'en émettre maximum 6 par cycle ! Et sur les CPU avec des fenêtres d'instruction décentralisées, il y a aussi une différence entre le nombre d'instructions qui sont ''dispatch'' et ''schedule''.
La catégorie la plus simple est celle des '''processeurs avec plusieurs pipelines'''. Ce sont des processeurs superscalaires sans exécution dans le désordre. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction.
Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, d'une FPU, éventuellement d'un second banc de registres, guère plus. Il permet d'exécuter une seconde opération de calcul, pas plus, ce qui est à l'origine de contraintes d'appariemment assez fortes. De plus, il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section.
===La double émission entière-flottante===
L'exemple le plus simple utilise un pipeline pour les opérations flottantes, séparé du pipeline originel. Pour bien expliquer les choses, prenons le processeur illustré ci-dessous. Un processeur a deux ''back-end'' séparés, un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par l'ALU entière, passent éventuellement dans l'unité mémoire, pour ensuite arriver à l'étape de ''writeback''. La FPU et le banc de registre flottant ont eux aussi leur propre ''back-end'' séparé.
[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]]
Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaqaue pipeline, à chaque cycle.
L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine.
Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants.
===La double émission entière===
Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait.
Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
==Les processeurs superscalaires dynamiques==
Les processeurs précédents, avec 2 pipelines, avaient une unité d'émission assez simple. Elle avait deux ports de décodage, et deux ports d'émission. Le nombre de ports de décodage et d'émission étaient identiques. Vous vous dites que c'est naturel, et intuitif, que c'est normal pour un processeur superscalaire. Mais en réalité, la majorité des processeurs superscalaires a plus de ports d'émission que de décodage. Expliquer pourquoi va cependant nous demander de parler de comment la superscalarité exploite les unités de calcul.
Posons-nous cette question : de combien d'unités un processeur superscalaire a besoin ? Les unités en question ne sont nécessairement des unité de calcul, l'unité mémoire et l'unité de branchement comptent aussi. La réponse dépend du nombre de voies. Si le ''front-end'' charge et décode un paquet de N instructions, exécuter ces N instructions simultanément demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section.
La réponse dépend aussi des contraintes d’appariement. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission parfaite devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement, ce qui ferait passer de 6 unités à 12. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, le cout en circuit encore plus prohibitif !
En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée, alors que la double émission parait plus adaptée.
Et c'est là que la différence entre ports d'émission et de décodage devient intéressante. L'idée est d'avoir une unité d'émission avec deux ports de décodage, et 4 port d'émission (un par unité). Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux micro-opérations tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu.
Les tout premiers CPU superscalaires étaient nombreux à utiliser cette technique. Les exemples classiques sont l'HyperSPARC, de l'alpha 21064 et du PowerPC 603. C'étaient des CPU double émission, avec une ALU entière, une unité mémoire et une FPU. Nous parlerons de '''partitionnement INT-FLOAT-MEM'''. Les contraintes d’appariement variaient grandement suivant le processeur.
Le PowerPC 603 implémentait une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur.
Avant de poursuivre, précisons que la FPU regroupe un additionneur flottant et un multiplieur flottant. Et ce fait peut être exploité pour approfondir le partitionnement. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions).
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple, facile à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. La raison est que le partitionnement permet d'émettre des paires, triplets, quadruplets de micro-opérations qui sont rares en pratique.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''.
Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, 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. Et enfin, il faut rajouter un port d'émission pour pouvoir émettre une micro-opération entière en plus.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle.
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
Reste à connecter les unités de calcul à l'unité d'émission (indirectement, car il y a des étages de pipeline entre les deux). Une implémentation basique utilisee un port d'émission par unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La présence d'un circuit multiplieur et d'un ''barrel shifter'' pose des problèmes pour le partitionnement. Il est possible d'avoir un port d'émission dédié au multiplieur, et un autre au ''barrel shifter'', qui sont séparés de ceux pour l'ALU entière. Il est alors possible d'émettre une opération entière, une multiplication et un décalage, simultanément. Nous parlerons de '''partitionnement entier''' pour désigner cette possibilité.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
Le partitionnement entier est cependant très rare, car le gain en performance serait trop faible au regard des couts. Quelques processeurs historiques l'utilisaient, mais ce n'est plus le cas de nos jours. La raison est que les multiplications et les décalages sont assez rares dans le code, ce qui fait que le port d'émission associés sont sous-utilisés. Et un port d'émission a un cout en circuit assez important. 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.
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul. L'exemple classique est justement lié aux circuits multiplieur et ''barrel shifters''. L'idée est que le multiplieur et le ''barrel shifter'' sont mis sur le même port d'émission qu'une ALU entière.
Prenons par exemple le processeur à double émission entière illustré ci-contre. Un port d'émission est relié à une ALU entière seule, et l'autre port d'émission alimente la seconde ALU entière et le multiplieur. La conséquence est que le processeur peut émettre deux opérations entières, dont une multiplication. Si on avait utilisé un port d'émission dédié au multiplieur, il aurait été possible d'émettre deux opérations entières et une multiplication. On perd donc une voie, mais on gagne en flexibilité et on économise des circuits.
[[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]]
En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais le partage des ports d'émission peut appliquer à n'importe quelle unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. 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.
La solution est de partager des ports d'émission, pour passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple relie une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites. Le CPU peut émettre 6 opérations entières simultanément, grâce à 6 ALU entières. Les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires mélangent les trois techniques===
Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autre unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents.
* Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU.
* La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants.
* La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables.
* Le '''partage des ports d'émission''' réduit le cout en circuit du CPU, pour des pertes de performances mineures si bien fait.
De nos jours, le partitionnement est utilisé en complément de la duplication des ALU entières, de la duplication des autres unités, et du partage des ports d'émission. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. Le partage des ports d'émission a lui aussi mis un peu de temps pour s'imposer.
Les CPU superscalaires tendent à mettre le paquet niveau opérations arithmétiques, mais pas forcément au point de remplir la totalité de la largeur du processeur. La règle est que pour N voies, le processeur dispose d'au moins N/2 ALU entières. Et N/2 est une borne basse, la plupart des CPU x86 ont environ tournent plutôt autour des 2/3, voire des 3/4. Attention, cela ne signifie pas que le processeur émet systématiquement N/2 opération entières, il peut par exemple remplacer des opérations entières par des opérations flottantes.
En général, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. La conséquence est que les processeurs ont commencé à dupliquer leurs unités mémoire dès qu'ils ont atteint la quadruple émission, pour les CPU grand public. Par exemple, la microarchitecture ''Golden Cove'' d'Intel dispose de cinq unités de calcul, trois unités mémoire (deux spécialisées dans les écritures, trois dans les lectures). La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire. Notez que le ratio est équilibré entre opérations entières et mémoire, avec soit égalité, soit un avantage pour les ALU entières.
Une autre régularité est que la largeur totale du processeur peut être occupée par des opérations entières/mémoire. Pour le dire autrement, sur un processeur quadruple émission, il pourra émettre un mix de 4 instructions entière/mémoire, avec zéro opération flottante. Idem avec des processeurs triples ou pentuple émission, voire plus. Si le processeur émet une opération flottante, elle remplace une opération entière.
Prenons l'exemple des 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. C'était des processeurs quadruple émission, ce qui fait que la moitié de la largeur du processeur sert pour des opérations entières, et l'autre moitié pour les accès mémoire.
{|class="wikitable"
|-
! Port 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
|}
Les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon, sont un exemple moins représentatif. Ces microarchitectures utilisent le partage des ports d'émission au maximum. C'était des microarchitectures à triple émission, avec six ports d’émission. Il y avait trois ports d'émission pour les opérations flottantes, et trois ports d'émission pour les micro-opérations entières/mémoire. Il y avait trois ALU entières et trois unités mémoire, chaque ALU partageait un port d'émission avec une unité mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements.
{|class="wikitable"
|-
! Port 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 || ALU entière || FADD || FMUL || FPU (autres opérations)
|-
| Multiplieur || || || || ||
|-
| LOAD/STORE || LOAD/STORE || LOAD/STORE || || ||
|}
Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair.
Par exemple, la microarchitecture ''Golden Cove'' a deux ''barrel shifters''.
{|class="wikitable"
|-
! Port 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 !! Port d'émission n°7 !! Port d'émission n°8 !! Port d'émission n°9 !! Port d'émission n°10
|-
| ALU entière || ALU entière || ALU entière || ALU entière || ALU entière || LOAD || LOAD || LOAD || STORE || STORE
|-
| ''Barrel shifter'' || Multiplieur || Multiplieur || ''Barrel shifter'' || || || || || ||
|-
| Branchements || Diviseur || || Branchements || || || || || ||
|}
==L'évolution historique de la superscalarité==
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. La plupart des CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 sont rapidement montés à de la quadruple émission, avec quelques exceptions. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 ||
|-
! 1995
| 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000
|-
! 1996
| class="f_bleu | 620 || || || || class="f_bleu | 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
===Les processeurs historiques à double émission===
Les premiers CPU à double émission sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Voici le tableau chronologique disant quel CPU utilisait quelle technique.
* Les CPU en vert utilisaient la double émission entière-flottante.
* Les CPU en rouge utilisaient la double émission entière.
* Les CPU en jaune utilisaient un ''partitionnement complet'' entre ALU, FPU et unité mémoire.
* Les CPU sans couleur utilisaient un grand nombre d'unités, avec beaucoup de ports d'émission.
* Les CPU barrés utilisent la triple ou quadruple émission et seront vu dans la section suivante.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| class="f_vert | RS/6000 || || || class="f_rouge | i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| class="f_vert | RSC || class="f_vert | PA 7100 || <s>SuperSPARC</s> || || class="f_jaune | 21064 ||
|-
! 1993
| class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || ||
|-
! 1994
| <s>POWER 2</s> || PA 7200 || || || <s>21164</s> ||
|}
Le PA 7200 est un PA-7100 auquel on aurait rajouté une ALU entière. Le processeur exécutait les multiplications entières dans la FPU, comme son prédécesseur. Les combinaisons autorisées étaient donc INT + INT, INT + MEM, INT + FLOAT/MUL, MEM + FLOAT/MUL.
===Les processeurs historiques à quadruple émission===
C'est la même année , en 1994, que la quadruple émission est arrivée et que les CPU ont commencé à avoir de plus en plus d'unités de calcul. Même les designs restés en double émission avaient ajouté des unités de calcul. Il y avait alors trois stratégies pour la quadruple émission.
La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Les FPUs étaient soit dupliquées, soit scindées en deux en séparant l'additionneur du multiplieur flottant. Le POWER 2 dupliquait ses FPUs à la perfection, alors que le 21264 partitionnait la FPU. Les accès mémoire étaient traités comme des opérations entières, et le processeur pouvait : soit en faire un seul par cycle, soit en émettre deux, sous conditions (le cache devait être double port pour lire/écrire deux données à la fois, comme on le verra dans la suite du chapitre).
La seconde implémentation dupliquait l'ALU entière et combinait le tout avec un partitionnement INT-FLOAT-MEM. De quoi exécuter en même temps deux opérations entières, une opération flottante, et un accès mémoire. 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).
Le SuperSPARC était un CPU triple émission, avec deux ALU entières, une FPU et une unité mémoire. Il avait de nombreuses contraintes d’appariement. Par exemple, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Une bonne partie de ces contraintes était lié au banc de registre, qui n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions. Il faut noter que la seconde ALU a été abandonnée lors du passage au CPU HyperSPARC, qui est de plus repassé en double émission, ce qui est une évolution à rebours de l'évolution des autres CPUs.
Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, avec au minimum deux ALU entières. Pour comprendre ce que je veux dire, prenons l'exemple de l'UltraSPARC. Il avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplier flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes. Le processeur pouvait émettre 4 instructions, tant qu'elles atterrissaient dans des unités différentes.
La même méthode peut être utilisée pour la double émission. Le Motorola 88110, sorti en 1991, était un CPU à double émission avec pas moins de 10 unités : deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise.
Les processeurs MIPS R8000 et R10000 avaient une implémentation plus moderne, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités. Moitié opérations entières, moitié opérations mémoire, avec de quoi remplacer les opérations entières par des opérations flottantes.
La morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes. Le tout est résumé dans le tableau ci-dessous, avec :
* en vert les CPU utilisant la première stratégie ;
* en rouge les CPU utilisant la seconde stratégie ;
* en jaune les CPU utilisant la troisième stratégie.
{|class="wikitable"
|+ Processeurs quadruple émission historiques
|-
! Année !! Processeur !! Unités
|-
! 1991
| class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques
|-
|-
! rowspan="2" | 1994
| class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'')
|-
| class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM
|-
! rowspan="3" | 1995
| PA 8000 || 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD
|-
| class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques
|-
| MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM
|-
! rowspan="2" | 1996
| class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM
|-
| class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL)
|}
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les CPU superscalaire à exécution dans le désordre==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Les unités d'exécution dans le désordre===
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports de ''dispatch''. Mais avec des stations de réservation, il faut le double des ports de ''schedule''. La raison est que les registres sont lus après émission avec une fenê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 de ''schedule'' que de ''dispatch''.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de 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===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Avec plusieurs dizaines d'unités de calcul différentes, ajouter autant 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.
La première solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Le processeur détecte quand il n'y a pas assez de ports pour servir toutes les micro-opérations. Quand ça arrive, l'unité d'émission émet assez de micro-opérations pour exploiter les ports disponibles, mais met en attente les micro-opérations en trop, le temps que les ports se libèrent. Pour cela, le banc de registre est géré par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Une autre solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une dernière solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
71btxj4pct907lhu9tetfxchm60vw7u
773205
773204
2026-09-25T21:07:56Z
Mewtow
31375
/* Les processeurs historiques à quadruple émission */
773205
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===L'impact des contraintes d’appariement sur la performance===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour la suite du chapitre===
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==Le ''front-end'' des processeurs superscalaires==
Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple.
Intuitivement, charger, décoder et 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 certaines processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. 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
|}
Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs.
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===L'émission parallèle des instructions===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elle sont propagée au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage).
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
==Les processeurs implémentés avec plusieurs pipelines==
Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Vous vous dites que l'implémentation d'un processeur superscalaire n'est pas la même suivant que le CPU utilise l'exécution dans le désordre, ou non. Mais dans les faits, la distinction est plus subtile que ca. Pour l'expliquer, je vais utiliser une classification originale, qui sépare les processeurs superscalaires en trois sous-types. La distinction se fait sur la manière dont la superscalarité est implémentée en matériel.La classification que j'utilise distingue :
* Les processeurs avec plusieurs pipelines, sans exécution dans le désordre.
* Les processeurs superscalaires avec un pipeline dynamique.
* Les processeurs avec des fenêtres d'instruction décentralisées.
La distinction n'est pas qu'une question d'implémentation matérielle. Elle impacte aussi le ratio entre instructions chargées/décodées et instructions émises. Vous vous dites que les deux sont égales, mais ce n'est le cas que sur les processeurs avec N pipelines ! Sur les autres processeurs, il est possible de décoder 4 instructions, mais d'en émettre maximum 6 par cycle ! Et sur les CPU avec des fenêtres d'instruction décentralisées, il y a aussi une différence entre le nombre d'instructions qui sont ''dispatch'' et ''schedule''.
La catégorie la plus simple est celle des '''processeurs avec plusieurs pipelines'''. Ce sont des processeurs superscalaires sans exécution dans le désordre. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction.
Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, d'une FPU, éventuellement d'un second banc de registres, guère plus. Il permet d'exécuter une seconde opération de calcul, pas plus, ce qui est à l'origine de contraintes d'appariemment assez fortes. De plus, il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section.
===La double émission entière-flottante===
L'exemple le plus simple utilise un pipeline pour les opérations flottantes, séparé du pipeline originel. Pour bien expliquer les choses, prenons le processeur illustré ci-dessous. Un processeur a deux ''back-end'' séparés, un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par l'ALU entière, passent éventuellement dans l'unité mémoire, pour ensuite arriver à l'étape de ''writeback''. La FPU et le banc de registre flottant ont eux aussi leur propre ''back-end'' séparé.
[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]]
Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaqaue pipeline, à chaque cycle.
L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine.
Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants.
===La double émission entière===
Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait.
Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
==Les processeurs superscalaires dynamiques==
Les processeurs précédents, avec 2 pipelines, avaient une unité d'émission assez simple. Elle avait deux ports de décodage, et deux ports d'émission. Le nombre de ports de décodage et d'émission étaient identiques. Vous vous dites que c'est naturel, et intuitif, que c'est normal pour un processeur superscalaire. Mais en réalité, la majorité des processeurs superscalaires a plus de ports d'émission que de décodage. Expliquer pourquoi va cependant nous demander de parler de comment la superscalarité exploite les unités de calcul.
Posons-nous cette question : de combien d'unités un processeur superscalaire a besoin ? Les unités en question ne sont nécessairement des unité de calcul, l'unité mémoire et l'unité de branchement comptent aussi. La réponse dépend du nombre de voies. Si le ''front-end'' charge et décode un paquet de N instructions, exécuter ces N instructions simultanément demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section.
La réponse dépend aussi des contraintes d’appariement. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission parfaite devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement, ce qui ferait passer de 6 unités à 12. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, le cout en circuit encore plus prohibitif !
En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée, alors que la double émission parait plus adaptée.
Et c'est là que la différence entre ports d'émission et de décodage devient intéressante. L'idée est d'avoir une unité d'émission avec deux ports de décodage, et 4 port d'émission (un par unité). Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux micro-opérations tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu.
Les tout premiers CPU superscalaires étaient nombreux à utiliser cette technique. Les exemples classiques sont l'HyperSPARC, de l'alpha 21064 et du PowerPC 603. C'étaient des CPU double émission, avec une ALU entière, une unité mémoire et une FPU. Nous parlerons de '''partitionnement INT-FLOAT-MEM'''. Les contraintes d’appariement variaient grandement suivant le processeur.
Le PowerPC 603 implémentait une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur.
Avant de poursuivre, précisons que la FPU regroupe un additionneur flottant et un multiplieur flottant. Et ce fait peut être exploité pour approfondir le partitionnement. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions).
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple, facile à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. La raison est que le partitionnement permet d'émettre des paires, triplets, quadruplets de micro-opérations qui sont rares en pratique.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''.
Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, 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. Et enfin, il faut rajouter un port d'émission pour pouvoir émettre une micro-opération entière en plus.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle.
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
Reste à connecter les unités de calcul à l'unité d'émission (indirectement, car il y a des étages de pipeline entre les deux). Une implémentation basique utilisee un port d'émission par unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La présence d'un circuit multiplieur et d'un ''barrel shifter'' pose des problèmes pour le partitionnement. Il est possible d'avoir un port d'émission dédié au multiplieur, et un autre au ''barrel shifter'', qui sont séparés de ceux pour l'ALU entière. Il est alors possible d'émettre une opération entière, une multiplication et un décalage, simultanément. Nous parlerons de '''partitionnement entier''' pour désigner cette possibilité.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
Le partitionnement entier est cependant très rare, car le gain en performance serait trop faible au regard des couts. Quelques processeurs historiques l'utilisaient, mais ce n'est plus le cas de nos jours. La raison est que les multiplications et les décalages sont assez rares dans le code, ce qui fait que le port d'émission associés sont sous-utilisés. Et un port d'émission a un cout en circuit assez important. 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.
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul. L'exemple classique est justement lié aux circuits multiplieur et ''barrel shifters''. L'idée est que le multiplieur et le ''barrel shifter'' sont mis sur le même port d'émission qu'une ALU entière.
Prenons par exemple le processeur à double émission entière illustré ci-contre. Un port d'émission est relié à une ALU entière seule, et l'autre port d'émission alimente la seconde ALU entière et le multiplieur. La conséquence est que le processeur peut émettre deux opérations entières, dont une multiplication. Si on avait utilisé un port d'émission dédié au multiplieur, il aurait été possible d'émettre deux opérations entières et une multiplication. On perd donc une voie, mais on gagne en flexibilité et on économise des circuits.
[[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]]
En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais le partage des ports d'émission peut appliquer à n'importe quelle unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. 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.
La solution est de partager des ports d'émission, pour passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple relie une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites. Le CPU peut émettre 6 opérations entières simultanément, grâce à 6 ALU entières. Les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires mélangent les trois techniques===
Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autre unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents.
* Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU.
* La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants.
* La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables.
* Le '''partage des ports d'émission''' réduit le cout en circuit du CPU, pour des pertes de performances mineures si bien fait.
De nos jours, le partitionnement est utilisé en complément de la duplication des ALU entières, de la duplication des autres unités, et du partage des ports d'émission. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. Le partage des ports d'émission a lui aussi mis un peu de temps pour s'imposer.
Les CPU superscalaires tendent à mettre le paquet niveau opérations arithmétiques, mais pas forcément au point de remplir la totalité de la largeur du processeur. La règle est que pour N voies, le processeur dispose d'au moins N/2 ALU entières. Et N/2 est une borne basse, la plupart des CPU x86 ont environ tournent plutôt autour des 2/3, voire des 3/4. Attention, cela ne signifie pas que le processeur émet systématiquement N/2 opération entières, il peut par exemple remplacer des opérations entières par des opérations flottantes.
En général, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. La conséquence est que les processeurs ont commencé à dupliquer leurs unités mémoire dès qu'ils ont atteint la quadruple émission, pour les CPU grand public. Par exemple, la microarchitecture ''Golden Cove'' d'Intel dispose de cinq unités de calcul, trois unités mémoire (deux spécialisées dans les écritures, trois dans les lectures). La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire. Notez que le ratio est équilibré entre opérations entières et mémoire, avec soit égalité, soit un avantage pour les ALU entières.
Une autre régularité est que la largeur totale du processeur peut être occupée par des opérations entières/mémoire. Pour le dire autrement, sur un processeur quadruple émission, il pourra émettre un mix de 4 instructions entière/mémoire, avec zéro opération flottante. Idem avec des processeurs triples ou pentuple émission, voire plus. Si le processeur émet une opération flottante, elle remplace une opération entière.
Prenons l'exemple des 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. C'était des processeurs quadruple émission, ce qui fait que la moitié de la largeur du processeur sert pour des opérations entières, et l'autre moitié pour les accès mémoire.
{|class="wikitable"
|-
! Port 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
|}
Les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon, sont un exemple moins représentatif. Ces microarchitectures utilisent le partage des ports d'émission au maximum. C'était des microarchitectures à triple émission, avec six ports d’émission. Il y avait trois ports d'émission pour les opérations flottantes, et trois ports d'émission pour les micro-opérations entières/mémoire. Il y avait trois ALU entières et trois unités mémoire, chaque ALU partageait un port d'émission avec une unité mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements.
{|class="wikitable"
|-
! Port 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 || ALU entière || FADD || FMUL || FPU (autres opérations)
|-
| Multiplieur || || || || ||
|-
| LOAD/STORE || LOAD/STORE || LOAD/STORE || || ||
|}
Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair.
Par exemple, la microarchitecture ''Golden Cove'' a deux ''barrel shifters''.
{|class="wikitable"
|-
! Port 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 !! Port d'émission n°7 !! Port d'émission n°8 !! Port d'émission n°9 !! Port d'émission n°10
|-
| ALU entière || ALU entière || ALU entière || ALU entière || ALU entière || LOAD || LOAD || LOAD || STORE || STORE
|-
| ''Barrel shifter'' || Multiplieur || Multiplieur || ''Barrel shifter'' || || || || || ||
|-
| Branchements || Diviseur || || Branchements || || || || || ||
|}
==L'évolution historique de la superscalarité==
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. La plupart des CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 sont rapidement montés à de la quadruple émission, avec quelques exceptions. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 ||
|-
! 1995
| 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000
|-
! 1996
| class="f_bleu | 620 || || || || class="f_bleu | 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
===Les processeurs historiques à double émission===
Les premiers CPU à double émission sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Voici le tableau chronologique disant quel CPU utilisait quelle technique.
* Les CPU en vert utilisaient la double émission entière-flottante.
* Les CPU en rouge utilisaient la double émission entière.
* Les CPU en jaune utilisaient un ''partitionnement complet'' entre ALU, FPU et unité mémoire.
* Les CPU sans couleur utilisaient un grand nombre d'unités, avec beaucoup de ports d'émission.
* Les CPU barrés utilisent la triple ou quadruple émission et seront vu dans la section suivante.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| class="f_vert | RS/6000 || || || class="f_rouge | i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| class="f_vert | RSC || class="f_vert | PA 7100 || <s>SuperSPARC</s> || || class="f_jaune | 21064 ||
|-
! 1993
| class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || ||
|-
! 1994
| <s>POWER 2</s> || PA 7200 || || || <s>21164</s> ||
|}
Le PA 7200 est un PA-7100 auquel on aurait rajouté une ALU entière. Le processeur exécutait les multiplications entières dans la FPU, comme son prédécesseur. Les combinaisons autorisées étaient donc INT + INT, INT + MEM, INT + FLOAT/MUL, MEM + FLOAT/MUL.
===Les processeurs historiques à quadruple émission===
C'est la même année , en 1994, que la quadruple émission est arrivée et que les CPU ont commencé à avoir de plus en plus d'unités de calcul. Même les designs restés en double émission avaient ajouté des unités de calcul. Il y avait alors trois stratégies pour la quadruple émission.
La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Les FPUs étaient soit dupliquées, soit scindées en deux en séparant l'additionneur du multiplieur flottant. Le POWER 2 dupliquait ses FPUs à la perfection, alors que le 21264 partitionnait la FPU. Les accès mémoire étaient traités comme des opérations entières, et le processeur pouvait : soit en faire un seul par cycle, soit en émettre deux, sous conditions (le cache devait être double port pour lire/écrire deux données à la fois, comme on le verra dans la suite du chapitre).
La seconde implémentation utilisait deux ALU entières, une unité mémoire et une FPU. De quoi exécuter en même temps deux opérations entières, une opération flottante, et un accès mémoire. La seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur). Un exemple est le SuperSPARC, un CPU triple émission. Il avait de nombreuses contraintes d’appariement. Par exemple, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Une bonne partie de ces contraintes était lié au banc de registre, qui n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions. Il faut noter que la seconde ALU a été abandonnée lors du passage au CPU HyperSPARC, qui est de plus repassé en double émission, ce qui est une évolution à rebours de l'évolution des autres CPUs.
Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, avec au minimum deux ALU entières. Pour comprendre ce que je veux dire, prenons l'exemple de l'UltraSPARC. Il avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplier flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes. Le processeur pouvait émettre 4 instructions, tant qu'elles atterrissaient dans des unités différentes.
La même méthode peut être utilisée pour la double émission. Le Motorola 88110, sorti en 1991, était un CPU à double émission avec pas moins de 10 unités : deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise.
Une quatrième méthode dupliquait tout ce qui pouvait l'être. Les processeurs MIPS R8000 et R10000 avaient une implémentation plus moderne, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités. Moitié opérations entières, moitié opérations mémoire, avec de quoi remplacer les opérations entières par des opérations flottantes. Le PA 8000, lui, dupliquait toutes ses unités en deux exemplaires, ce qui n'est pas l'idéal, aucun processeur moderne ne fait cela aujourd'hui.
La morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes. Le tout est résumé dans le tableau ci-dessous, avec :
* en vert les CPU utilisant la première stratégie ;
* en rouge les CPU utilisant la seconde stratégie ;
* en jaune les CPU utilisant la troisième stratégie.
{|class="wikitable"
|+ Processeurs quadruple émission historiques
|-
! Année !! Processeur !! Unités
|-
! 1991
| class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques
|-
|-
! rowspan="2" | 1994
| class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'')
|-
| class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM
|-
! rowspan="3" | 1995
| PA 8000 || 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD
|-
| class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques
|-
| MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM
|-
! rowspan="2" | 1996
| class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM
|-
| class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL)
|}
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les CPU superscalaire à exécution dans le désordre==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Les unités d'exécution dans le désordre===
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports de ''dispatch''. Mais avec des stations de réservation, il faut le double des ports de ''schedule''. La raison est que les registres sont lus après émission avec une fenê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 de ''schedule'' que de ''dispatch''.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de 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===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Avec plusieurs dizaines d'unités de calcul différentes, ajouter autant 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.
La première solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Le processeur détecte quand il n'y a pas assez de ports pour servir toutes les micro-opérations. Quand ça arrive, l'unité d'émission émet assez de micro-opérations pour exploiter les ports disponibles, mais met en attente les micro-opérations en trop, le temps que les ports se libèrent. Pour cela, le banc de registre est géré par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Une autre solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une dernière solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
g2h3ikt9kdfh8j9n9fqvsj92ptja27v
773206
773205
2026-09-25T21:10:05Z
Mewtow
31375
/* Les processeurs historiques à quadruple émission */
773206
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===L'impact des contraintes d’appariement sur la performance===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour la suite du chapitre===
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==Le ''front-end'' des processeurs superscalaires==
Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple.
Intuitivement, charger, décoder et 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 certaines processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. 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
|}
Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs.
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===L'émission parallèle des instructions===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elle sont propagée au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage).
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
==Les processeurs implémentés avec plusieurs pipelines==
Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Vous vous dites que l'implémentation d'un processeur superscalaire n'est pas la même suivant que le CPU utilise l'exécution dans le désordre, ou non. Mais dans les faits, la distinction est plus subtile que ca. Pour l'expliquer, je vais utiliser une classification originale, qui sépare les processeurs superscalaires en trois sous-types. La distinction se fait sur la manière dont la superscalarité est implémentée en matériel.La classification que j'utilise distingue :
* Les processeurs avec plusieurs pipelines, sans exécution dans le désordre.
* Les processeurs superscalaires avec un pipeline dynamique.
* Les processeurs avec des fenêtres d'instruction décentralisées.
La distinction n'est pas qu'une question d'implémentation matérielle. Elle impacte aussi le ratio entre instructions chargées/décodées et instructions émises. Vous vous dites que les deux sont égales, mais ce n'est le cas que sur les processeurs avec N pipelines ! Sur les autres processeurs, il est possible de décoder 4 instructions, mais d'en émettre maximum 6 par cycle ! Et sur les CPU avec des fenêtres d'instruction décentralisées, il y a aussi une différence entre le nombre d'instructions qui sont ''dispatch'' et ''schedule''.
La catégorie la plus simple est celle des '''processeurs avec plusieurs pipelines'''. Ce sont des processeurs superscalaires sans exécution dans le désordre. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction.
Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, d'une FPU, éventuellement d'un second banc de registres, guère plus. Il permet d'exécuter une seconde opération de calcul, pas plus, ce qui est à l'origine de contraintes d'appariemment assez fortes. De plus, il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section.
===La double émission entière-flottante===
L'exemple le plus simple utilise un pipeline pour les opérations flottantes, séparé du pipeline originel. Pour bien expliquer les choses, prenons le processeur illustré ci-dessous. Un processeur a deux ''back-end'' séparés, un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par l'ALU entière, passent éventuellement dans l'unité mémoire, pour ensuite arriver à l'étape de ''writeback''. La FPU et le banc de registre flottant ont eux aussi leur propre ''back-end'' séparé.
[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]]
Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaqaue pipeline, à chaque cycle.
L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine.
Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants.
===La double émission entière===
Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait.
Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
==Les processeurs superscalaires dynamiques==
Les processeurs précédents, avec 2 pipelines, avaient une unité d'émission assez simple. Elle avait deux ports de décodage, et deux ports d'émission. Le nombre de ports de décodage et d'émission étaient identiques. Vous vous dites que c'est naturel, et intuitif, que c'est normal pour un processeur superscalaire. Mais en réalité, la majorité des processeurs superscalaires a plus de ports d'émission que de décodage. Expliquer pourquoi va cependant nous demander de parler de comment la superscalarité exploite les unités de calcul.
Posons-nous cette question : de combien d'unités un processeur superscalaire a besoin ? Les unités en question ne sont nécessairement des unité de calcul, l'unité mémoire et l'unité de branchement comptent aussi. La réponse dépend du nombre de voies. Si le ''front-end'' charge et décode un paquet de N instructions, exécuter ces N instructions simultanément demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section.
La réponse dépend aussi des contraintes d’appariement. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission parfaite devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement, ce qui ferait passer de 6 unités à 12. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, le cout en circuit encore plus prohibitif !
En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée, alors que la double émission parait plus adaptée.
Et c'est là que la différence entre ports d'émission et de décodage devient intéressante. L'idée est d'avoir une unité d'émission avec deux ports de décodage, et 4 port d'émission (un par unité). Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux micro-opérations tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu.
Les tout premiers CPU superscalaires étaient nombreux à utiliser cette technique. Les exemples classiques sont l'HyperSPARC, de l'alpha 21064 et du PowerPC 603. C'étaient des CPU double émission, avec une ALU entière, une unité mémoire et une FPU. Nous parlerons de '''partitionnement INT-FLOAT-MEM'''. Les contraintes d’appariement variaient grandement suivant le processeur.
Le PowerPC 603 implémentait une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur.
Avant de poursuivre, précisons que la FPU regroupe un additionneur flottant et un multiplieur flottant. Et ce fait peut être exploité pour approfondir le partitionnement. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions).
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple, facile à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. La raison est que le partitionnement permet d'émettre des paires, triplets, quadruplets de micro-opérations qui sont rares en pratique.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''.
Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, 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. Et enfin, il faut rajouter un port d'émission pour pouvoir émettre une micro-opération entière en plus.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle.
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
Reste à connecter les unités de calcul à l'unité d'émission (indirectement, car il y a des étages de pipeline entre les deux). Une implémentation basique utilisee un port d'émission par unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La présence d'un circuit multiplieur et d'un ''barrel shifter'' pose des problèmes pour le partitionnement. Il est possible d'avoir un port d'émission dédié au multiplieur, et un autre au ''barrel shifter'', qui sont séparés de ceux pour l'ALU entière. Il est alors possible d'émettre une opération entière, une multiplication et un décalage, simultanément. Nous parlerons de '''partitionnement entier''' pour désigner cette possibilité.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
Le partitionnement entier est cependant très rare, car le gain en performance serait trop faible au regard des couts. Quelques processeurs historiques l'utilisaient, mais ce n'est plus le cas de nos jours. La raison est que les multiplications et les décalages sont assez rares dans le code, ce qui fait que le port d'émission associés sont sous-utilisés. Et un port d'émission a un cout en circuit assez important. 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.
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul. L'exemple classique est justement lié aux circuits multiplieur et ''barrel shifters''. L'idée est que le multiplieur et le ''barrel shifter'' sont mis sur le même port d'émission qu'une ALU entière.
Prenons par exemple le processeur à double émission entière illustré ci-contre. Un port d'émission est relié à une ALU entière seule, et l'autre port d'émission alimente la seconde ALU entière et le multiplieur. La conséquence est que le processeur peut émettre deux opérations entières, dont une multiplication. Si on avait utilisé un port d'émission dédié au multiplieur, il aurait été possible d'émettre deux opérations entières et une multiplication. On perd donc une voie, mais on gagne en flexibilité et on économise des circuits.
[[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]]
En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais le partage des ports d'émission peut appliquer à n'importe quelle unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. 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.
La solution est de partager des ports d'émission, pour passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple relie une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites. Le CPU peut émettre 6 opérations entières simultanément, grâce à 6 ALU entières. Les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires mélangent les trois techniques===
Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autre unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents.
* Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU.
* La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants.
* La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables.
* Le '''partage des ports d'émission''' réduit le cout en circuit du CPU, pour des pertes de performances mineures si bien fait.
De nos jours, le partitionnement est utilisé en complément de la duplication des ALU entières, de la duplication des autres unités, et du partage des ports d'émission. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. Le partage des ports d'émission a lui aussi mis un peu de temps pour s'imposer.
Les CPU superscalaires tendent à mettre le paquet niveau opérations arithmétiques, mais pas forcément au point de remplir la totalité de la largeur du processeur. La règle est que pour N voies, le processeur dispose d'au moins N/2 ALU entières. Et N/2 est une borne basse, la plupart des CPU x86 ont environ tournent plutôt autour des 2/3, voire des 3/4. Attention, cela ne signifie pas que le processeur émet systématiquement N/2 opération entières, il peut par exemple remplacer des opérations entières par des opérations flottantes.
En général, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. La conséquence est que les processeurs ont commencé à dupliquer leurs unités mémoire dès qu'ils ont atteint la quadruple émission, pour les CPU grand public. Par exemple, la microarchitecture ''Golden Cove'' d'Intel dispose de cinq unités de calcul, trois unités mémoire (deux spécialisées dans les écritures, trois dans les lectures). La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire. Notez que le ratio est équilibré entre opérations entières et mémoire, avec soit égalité, soit un avantage pour les ALU entières.
Une autre régularité est que la largeur totale du processeur peut être occupée par des opérations entières/mémoire. Pour le dire autrement, sur un processeur quadruple émission, il pourra émettre un mix de 4 instructions entière/mémoire, avec zéro opération flottante. Idem avec des processeurs triples ou pentuple émission, voire plus. Si le processeur émet une opération flottante, elle remplace une opération entière.
Prenons l'exemple des 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. C'était des processeurs quadruple émission, ce qui fait que la moitié de la largeur du processeur sert pour des opérations entières, et l'autre moitié pour les accès mémoire.
{|class="wikitable"
|-
! Port 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
|}
Les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon, sont un exemple moins représentatif. Ces microarchitectures utilisent le partage des ports d'émission au maximum. C'était des microarchitectures à triple émission, avec six ports d’émission. Il y avait trois ports d'émission pour les opérations flottantes, et trois ports d'émission pour les micro-opérations entières/mémoire. Il y avait trois ALU entières et trois unités mémoire, chaque ALU partageait un port d'émission avec une unité mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements.
{|class="wikitable"
|-
! Port 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 || ALU entière || FADD || FMUL || FPU (autres opérations)
|-
| Multiplieur || || || || ||
|-
| LOAD/STORE || LOAD/STORE || LOAD/STORE || || ||
|}
Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair.
Par exemple, la microarchitecture ''Golden Cove'' a deux ''barrel shifters''.
{|class="wikitable"
|-
! Port 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 !! Port d'émission n°7 !! Port d'émission n°8 !! Port d'émission n°9 !! Port d'émission n°10
|-
| ALU entière || ALU entière || ALU entière || ALU entière || ALU entière || LOAD || LOAD || LOAD || STORE || STORE
|-
| ''Barrel shifter'' || Multiplieur || Multiplieur || ''Barrel shifter'' || || || || || ||
|-
| Branchements || Diviseur || || Branchements || || || || || ||
|}
==L'évolution historique de la superscalarité==
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. La plupart des CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 sont rapidement montés à de la quadruple émission, avec quelques exceptions. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 ||
|-
! 1995
| 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000
|-
! 1996
| class="f_bleu | 620 || || || || class="f_bleu | 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
===Les processeurs historiques à double émission===
Les premiers CPU à double émission sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Voici le tableau chronologique disant quel CPU utilisait quelle technique.
* Les CPU en vert utilisaient la double émission entière-flottante.
* Les CPU en rouge utilisaient la double émission entière.
* Les CPU en jaune utilisaient un ''partitionnement complet'' entre ALU, FPU et unité mémoire.
* Les CPU sans couleur utilisaient un grand nombre d'unités, avec beaucoup de ports d'émission.
* Les CPU barrés utilisent la triple ou quadruple émission et seront vu dans la section suivante.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| class="f_vert | RS/6000 || || || class="f_rouge | i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| class="f_vert | RSC || class="f_vert | PA 7100 || <s>SuperSPARC</s> || || class="f_jaune | 21064 ||
|-
! 1993
| class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || ||
|-
! 1994
| <s>POWER 2</s> || PA 7200 || || || <s>21164</s> ||
|}
Le PA 7200 est un PA-7100 auquel on aurait rajouté une ALU entière. Le processeur exécutait les multiplications entières dans la FPU, comme son prédécesseur. Les combinaisons autorisées étaient donc INT + INT, INT + MEM, INT + FLOAT/MUL, MEM + FLOAT/MUL.
===Les processeurs historiques à quadruple émission===
C'est la même année , en 1994, que la quadruple émission est arrivée et que les CPU ont commencé à avoir de plus en plus d'unités de calcul. Même les designs restés en double émission avaient ajouté des unités de calcul. Il y avait alors trois stratégies pour la quadruple émission.
La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Les FPUs étaient soit dupliquées, soit scindées en deux en séparant l'additionneur du multiplieur flottant. Le POWER 2 dupliquait ses FPUs à la perfection, alors que le 21264 partitionnait la FPU. Les accès mémoire étaient traités comme des opérations entières, et le processeur pouvait : soit en faire un seul par cycle, soit en émettre deux, sous conditions (le cache devait être double port pour lire/écrire deux données à la fois, comme on le verra dans la suite du chapitre).
La seconde implémentation utilisait deux ALU entières, une unité mémoire et une FPU. De quoi exécuter en même temps deux opérations entières, une opération flottante, et un accès mémoire. La seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur). Un exemple est le SuperSPARC, un CPU triple émission. Il avait de nombreuses contraintes d’appariement. Par exemple, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Une bonne partie de ces contraintes était lié au banc de registre, qui n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions. Il faut noter que la seconde ALU a été abandonnée lors du passage au CPU HyperSPARC, qui est de plus repassé en double émission, ce qui est une évolution à rebours de l'évolution des autres CPUs.
Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, avec au minimum deux ALU entières. Pour comprendre ce que je veux dire, prenons l'exemple de l'UltraSPARC. Il avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplier flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes. Le processeur pouvait émettre 4 instructions, tant qu'elles atterrissaient dans des unités différentes. Le Motorola 88110, vu en exemple plus haut, était un CPU à double émission avec pas moins de 10 unités.
Une quatrième méthode dupliquait tout ce qui pouvait l'être. Les processeurs MIPS R8000 et R10000 avaient une implémentation plus moderne, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités. Moitié opérations entières, moitié opérations mémoire, avec de quoi remplacer les opérations entières par des opérations flottantes. Le PA 8000, lui, dupliquait toutes ses unités en deux exemplaires, ce qui n'est pas l'idéal, aucun processeur moderne ne fait cela aujourd'hui.
La morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes. Le tout est résumé dans le tableau ci-dessous, avec :
* en vert les CPU utilisant la première stratégie ;
* en rouge les CPU utilisant la seconde stratégie ;
* en jaune les CPU utilisant la troisième stratégie.
{|class="wikitable"
|+ Processeurs quadruple émission historiques
|-
! Année !! Processeur !! Unités
|-
! 1991
| class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques
|-
|-
! rowspan="2" | 1994
| class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'')
|-
| class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM
|-
! rowspan="3" | 1995
| PA 8000 || 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD
|-
| class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques
|-
| MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM
|-
! rowspan="2" | 1996
| class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM
|-
| class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL)
|}
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les CPU superscalaire à exécution dans le désordre==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Les unités d'exécution dans le désordre===
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports de ''dispatch''. Mais avec des stations de réservation, il faut le double des ports de ''schedule''. La raison est que les registres sont lus après émission avec une fenê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 de ''schedule'' que de ''dispatch''.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de 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===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Avec plusieurs dizaines d'unités de calcul différentes, ajouter autant 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.
La première solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Le processeur détecte quand il n'y a pas assez de ports pour servir toutes les micro-opérations. Quand ça arrive, l'unité d'émission émet assez de micro-opérations pour exploiter les ports disponibles, mais met en attente les micro-opérations en trop, le temps que les ports se libèrent. Pour cela, le banc de registre est géré par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Une autre solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une dernière solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
nk8uq97qzb2j25dxknk6823xrldqvb7
773207
773206
2026-09-25T21:11:06Z
Mewtow
31375
/* La duplication des ALU entières */
773207
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===L'impact des contraintes d’appariement sur la performance===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour la suite du chapitre===
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==Le ''front-end'' des processeurs superscalaires==
Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple.
Intuitivement, charger, décoder et 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 certaines processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. 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
|}
Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs.
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===L'émission parallèle des instructions===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elle sont propagée au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage).
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
==Les processeurs implémentés avec plusieurs pipelines==
Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Vous vous dites que l'implémentation d'un processeur superscalaire n'est pas la même suivant que le CPU utilise l'exécution dans le désordre, ou non. Mais dans les faits, la distinction est plus subtile que ca. Pour l'expliquer, je vais utiliser une classification originale, qui sépare les processeurs superscalaires en trois sous-types. La distinction se fait sur la manière dont la superscalarité est implémentée en matériel.La classification que j'utilise distingue :
* Les processeurs avec plusieurs pipelines, sans exécution dans le désordre.
* Les processeurs superscalaires avec un pipeline dynamique.
* Les processeurs avec des fenêtres d'instruction décentralisées.
La distinction n'est pas qu'une question d'implémentation matérielle. Elle impacte aussi le ratio entre instructions chargées/décodées et instructions émises. Vous vous dites que les deux sont égales, mais ce n'est le cas que sur les processeurs avec N pipelines ! Sur les autres processeurs, il est possible de décoder 4 instructions, mais d'en émettre maximum 6 par cycle ! Et sur les CPU avec des fenêtres d'instruction décentralisées, il y a aussi une différence entre le nombre d'instructions qui sont ''dispatch'' et ''schedule''.
La catégorie la plus simple est celle des '''processeurs avec plusieurs pipelines'''. Ce sont des processeurs superscalaires sans exécution dans le désordre. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction.
Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, d'une FPU, éventuellement d'un second banc de registres, guère plus. Il permet d'exécuter une seconde opération de calcul, pas plus, ce qui est à l'origine de contraintes d'appariemment assez fortes. De plus, il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section.
===La double émission entière-flottante===
L'exemple le plus simple utilise un pipeline pour les opérations flottantes, séparé du pipeline originel. Pour bien expliquer les choses, prenons le processeur illustré ci-dessous. Un processeur a deux ''back-end'' séparés, un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par l'ALU entière, passent éventuellement dans l'unité mémoire, pour ensuite arriver à l'étape de ''writeback''. La FPU et le banc de registre flottant ont eux aussi leur propre ''back-end'' séparé.
[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]]
Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaqaue pipeline, à chaque cycle.
L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine.
Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants.
===La double émission entière===
Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait.
Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
==Les processeurs superscalaires dynamiques==
Les processeurs précédents, avec 2 pipelines, avaient une unité d'émission assez simple. Elle avait deux ports de décodage, et deux ports d'émission. Le nombre de ports de décodage et d'émission étaient identiques. Vous vous dites que c'est naturel, et intuitif, que c'est normal pour un processeur superscalaire. Mais en réalité, la majorité des processeurs superscalaires a plus de ports d'émission que de décodage. Expliquer pourquoi va cependant nous demander de parler de comment la superscalarité exploite les unités de calcul.
Posons-nous cette question : de combien d'unités un processeur superscalaire a besoin ? Les unités en question ne sont nécessairement des unité de calcul, l'unité mémoire et l'unité de branchement comptent aussi. La réponse dépend du nombre de voies. Si le ''front-end'' charge et décode un paquet de N instructions, exécuter ces N instructions simultanément demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section.
La réponse dépend aussi des contraintes d’appariement. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission parfaite devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement, ce qui ferait passer de 6 unités à 12. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, le cout en circuit encore plus prohibitif !
En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée, alors que la double émission parait plus adaptée.
Et c'est là que la différence entre ports d'émission et de décodage devient intéressante. L'idée est d'avoir une unité d'émission avec deux ports de décodage, et 4 port d'émission (un par unité). Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux micro-opérations tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu.
Les tout premiers CPU superscalaires étaient nombreux à utiliser cette technique. Les exemples classiques sont l'HyperSPARC, de l'alpha 21064 et du PowerPC 603. C'étaient des CPU double émission, avec une ALU entière, une unité mémoire et une FPU. Nous parlerons de '''partitionnement INT-FLOAT-MEM'''. Les contraintes d’appariement variaient grandement suivant le processeur.
Le PowerPC 603 implémentait une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur.
Avant de poursuivre, précisons que la FPU regroupe un additionneur flottant et un multiplieur flottant. Et ce fait peut être exploité pour approfondir le partitionnement. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions).
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple, facile à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. La raison est que le partitionnement permet d'émettre des paires, triplets, quadruplets de micro-opérations qui sont rares en pratique.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''.
Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, 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. Et enfin, il faut rajouter un port d'émission pour pouvoir émettre une micro-opération entière en plus.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle.
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
Reste à connecter les unités de calcul à l'unité d'émission (indirectement, car il y a des étages de pipeline entre les deux). Une implémentation basique utilisee un port d'émission par unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Et chaque unité a son propre port d'émission. La conséquence est que les contraintes d'appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La présence d'un circuit multiplieur et d'un ''barrel shifter'' pose des problèmes pour le partitionnement. Il est possible d'avoir un port d'émission dédié au multiplieur, et un autre au ''barrel shifter'', qui sont séparés de ceux pour l'ALU entière. Il est alors possible d'émettre une opération entière, une multiplication et un décalage, simultanément. Nous parlerons de '''partitionnement entier''' pour désigner cette possibilité.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
Le partitionnement entier est cependant très rare, car le gain en performance serait trop faible au regard des couts. Quelques processeurs historiques l'utilisaient, mais ce n'est plus le cas de nos jours. La raison est que les multiplications et les décalages sont assez rares dans le code, ce qui fait que le port d'émission associés sont sous-utilisés. Et un port d'émission a un cout en circuit assez important. 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.
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul. L'exemple classique est justement lié aux circuits multiplieur et ''barrel shifters''. L'idée est que le multiplieur et le ''barrel shifter'' sont mis sur le même port d'émission qu'une ALU entière.
Prenons par exemple le processeur à double émission entière illustré ci-contre. Un port d'émission est relié à une ALU entière seule, et l'autre port d'émission alimente la seconde ALU entière et le multiplieur. La conséquence est que le processeur peut émettre deux opérations entières, dont une multiplication. Si on avait utilisé un port d'émission dédié au multiplieur, il aurait été possible d'émettre deux opérations entières et une multiplication. On perd donc une voie, mais on gagne en flexibilité et on économise des circuits.
[[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]]
En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais le partage des ports d'émission peut appliquer à n'importe quelle unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. 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.
La solution est de partager des ports d'émission, pour passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple relie une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites. Le CPU peut émettre 6 opérations entières simultanément, grâce à 6 ALU entières. Les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires mélangent les trois techniques===
Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autre unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents.
* Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU.
* La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants.
* La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables.
* Le '''partage des ports d'émission''' réduit le cout en circuit du CPU, pour des pertes de performances mineures si bien fait.
De nos jours, le partitionnement est utilisé en complément de la duplication des ALU entières, de la duplication des autres unités, et du partage des ports d'émission. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. Le partage des ports d'émission a lui aussi mis un peu de temps pour s'imposer.
Les CPU superscalaires tendent à mettre le paquet niveau opérations arithmétiques, mais pas forcément au point de remplir la totalité de la largeur du processeur. La règle est que pour N voies, le processeur dispose d'au moins N/2 ALU entières. Et N/2 est une borne basse, la plupart des CPU x86 ont environ tournent plutôt autour des 2/3, voire des 3/4. Attention, cela ne signifie pas que le processeur émet systématiquement N/2 opération entières, il peut par exemple remplacer des opérations entières par des opérations flottantes.
En général, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. La conséquence est que les processeurs ont commencé à dupliquer leurs unités mémoire dès qu'ils ont atteint la quadruple émission, pour les CPU grand public. Par exemple, la microarchitecture ''Golden Cove'' d'Intel dispose de cinq unités de calcul, trois unités mémoire (deux spécialisées dans les écritures, trois dans les lectures). La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire. Notez que le ratio est équilibré entre opérations entières et mémoire, avec soit égalité, soit un avantage pour les ALU entières.
Une autre régularité est que la largeur totale du processeur peut être occupée par des opérations entières/mémoire. Pour le dire autrement, sur un processeur quadruple émission, il pourra émettre un mix de 4 instructions entière/mémoire, avec zéro opération flottante. Idem avec des processeurs triples ou pentuple émission, voire plus. Si le processeur émet une opération flottante, elle remplace une opération entière.
Prenons l'exemple des 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. C'était des processeurs quadruple émission, ce qui fait que la moitié de la largeur du processeur sert pour des opérations entières, et l'autre moitié pour les accès mémoire.
{|class="wikitable"
|-
! Port 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
|}
Les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon, sont un exemple moins représentatif. Ces microarchitectures utilisent le partage des ports d'émission au maximum. C'était des microarchitectures à triple émission, avec six ports d’émission. Il y avait trois ports d'émission pour les opérations flottantes, et trois ports d'émission pour les micro-opérations entières/mémoire. Il y avait trois ALU entières et trois unités mémoire, chaque ALU partageait un port d'émission avec une unité mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements.
{|class="wikitable"
|-
! Port 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 || ALU entière || FADD || FMUL || FPU (autres opérations)
|-
| Multiplieur || || || || ||
|-
| LOAD/STORE || LOAD/STORE || LOAD/STORE || || ||
|}
Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair.
Par exemple, la microarchitecture ''Golden Cove'' a deux ''barrel shifters''.
{|class="wikitable"
|-
! Port 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 !! Port d'émission n°7 !! Port d'émission n°8 !! Port d'émission n°9 !! Port d'émission n°10
|-
| ALU entière || ALU entière || ALU entière || ALU entière || ALU entière || LOAD || LOAD || LOAD || STORE || STORE
|-
| ''Barrel shifter'' || Multiplieur || Multiplieur || ''Barrel shifter'' || || || || || ||
|-
| Branchements || Diviseur || || Branchements || || || || || ||
|}
==L'évolution historique de la superscalarité==
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. La plupart des CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 sont rapidement montés à de la quadruple émission, avec quelques exceptions. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 ||
|-
! 1995
| 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000
|-
! 1996
| class="f_bleu | 620 || || || || class="f_bleu | 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
===Les processeurs historiques à double émission===
Les premiers CPU à double émission sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Voici le tableau chronologique disant quel CPU utilisait quelle technique.
* Les CPU en vert utilisaient la double émission entière-flottante.
* Les CPU en rouge utilisaient la double émission entière.
* Les CPU en jaune utilisaient un ''partitionnement complet'' entre ALU, FPU et unité mémoire.
* Les CPU sans couleur utilisaient un grand nombre d'unités, avec beaucoup de ports d'émission.
* Les CPU barrés utilisent la triple ou quadruple émission et seront vu dans la section suivante.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| class="f_vert | RS/6000 || || || class="f_rouge | i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| class="f_vert | RSC || class="f_vert | PA 7100 || <s>SuperSPARC</s> || || class="f_jaune | 21064 ||
|-
! 1993
| class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || ||
|-
! 1994
| <s>POWER 2</s> || PA 7200 || || || <s>21164</s> ||
|}
Le PA 7200 est un PA-7100 auquel on aurait rajouté une ALU entière. Le processeur exécutait les multiplications entières dans la FPU, comme son prédécesseur. Les combinaisons autorisées étaient donc INT + INT, INT + MEM, INT + FLOAT/MUL, MEM + FLOAT/MUL.
===Les processeurs historiques à quadruple émission===
C'est la même année , en 1994, que la quadruple émission est arrivée et que les CPU ont commencé à avoir de plus en plus d'unités de calcul. Même les designs restés en double émission avaient ajouté des unités de calcul. Il y avait alors trois stratégies pour la quadruple émission.
La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Les FPUs étaient soit dupliquées, soit scindées en deux en séparant l'additionneur du multiplieur flottant. Le POWER 2 dupliquait ses FPUs à la perfection, alors que le 21264 partitionnait la FPU. Les accès mémoire étaient traités comme des opérations entières, et le processeur pouvait : soit en faire un seul par cycle, soit en émettre deux, sous conditions (le cache devait être double port pour lire/écrire deux données à la fois, comme on le verra dans la suite du chapitre).
La seconde implémentation utilisait deux ALU entières, une unité mémoire et une FPU. De quoi exécuter en même temps deux opérations entières, une opération flottante, et un accès mémoire. La seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur). Un exemple est le SuperSPARC, un CPU triple émission. Il avait de nombreuses contraintes d’appariement. Par exemple, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Une bonne partie de ces contraintes était lié au banc de registre, qui n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions. Il faut noter que la seconde ALU a été abandonnée lors du passage au CPU HyperSPARC, qui est de plus repassé en double émission, ce qui est une évolution à rebours de l'évolution des autres CPUs.
Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, avec au minimum deux ALU entières. Pour comprendre ce que je veux dire, prenons l'exemple de l'UltraSPARC. Il avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplier flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes. Le processeur pouvait émettre 4 instructions, tant qu'elles atterrissaient dans des unités différentes. Le Motorola 88110, vu en exemple plus haut, était un CPU à double émission avec pas moins de 10 unités.
Une quatrième méthode dupliquait tout ce qui pouvait l'être. Les processeurs MIPS R8000 et R10000 avaient une implémentation plus moderne, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités. Moitié opérations entières, moitié opérations mémoire, avec de quoi remplacer les opérations entières par des opérations flottantes. Le PA 8000, lui, dupliquait toutes ses unités en deux exemplaires, ce qui n'est pas l'idéal, aucun processeur moderne ne fait cela aujourd'hui.
La morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes. Le tout est résumé dans le tableau ci-dessous, avec :
* en vert les CPU utilisant la première stratégie ;
* en rouge les CPU utilisant la seconde stratégie ;
* en jaune les CPU utilisant la troisième stratégie.
{|class="wikitable"
|+ Processeurs quadruple émission historiques
|-
! Année !! Processeur !! Unités
|-
! 1991
| class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques
|-
|-
! rowspan="2" | 1994
| class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'')
|-
| class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM
|-
! rowspan="3" | 1995
| PA 8000 || 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD
|-
| class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques
|-
| MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM
|-
! rowspan="2" | 1996
| class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM
|-
| class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL)
|}
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les CPU superscalaire à exécution dans le désordre==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Les unités d'exécution dans le désordre===
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports de ''dispatch''. Mais avec des stations de réservation, il faut le double des ports de ''schedule''. La raison est que les registres sont lus après émission avec une fenê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 de ''schedule'' que de ''dispatch''.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de 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===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Avec plusieurs dizaines d'unités de calcul différentes, ajouter autant 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.
La première solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Le processeur détecte quand il n'y a pas assez de ports pour servir toutes les micro-opérations. Quand ça arrive, l'unité d'émission émet assez de micro-opérations pour exploiter les ports disponibles, mais met en attente les micro-opérations en trop, le temps que les ports se libèrent. Pour cela, le banc de registre est géré par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Une autre solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une dernière solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
lqnrlh3oootvuxkcmwa6r9bhss4wrr7
773208
773207
2026-09-25T21:12:47Z
Mewtow
31375
/* Les processeurs historiques à double émission */
773208
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===L'impact des contraintes d’appariement sur la performance===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour la suite du chapitre===
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==Le ''front-end'' des processeurs superscalaires==
Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple.
Intuitivement, charger, décoder et 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 certaines processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. 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
|}
Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs.
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===L'émission parallèle des instructions===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elle sont propagée au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage).
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
==Les processeurs implémentés avec plusieurs pipelines==
Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Vous vous dites que l'implémentation d'un processeur superscalaire n'est pas la même suivant que le CPU utilise l'exécution dans le désordre, ou non. Mais dans les faits, la distinction est plus subtile que ca. Pour l'expliquer, je vais utiliser une classification originale, qui sépare les processeurs superscalaires en trois sous-types. La distinction se fait sur la manière dont la superscalarité est implémentée en matériel.La classification que j'utilise distingue :
* Les processeurs avec plusieurs pipelines, sans exécution dans le désordre.
* Les processeurs superscalaires avec un pipeline dynamique.
* Les processeurs avec des fenêtres d'instruction décentralisées.
La distinction n'est pas qu'une question d'implémentation matérielle. Elle impacte aussi le ratio entre instructions chargées/décodées et instructions émises. Vous vous dites que les deux sont égales, mais ce n'est le cas que sur les processeurs avec N pipelines ! Sur les autres processeurs, il est possible de décoder 4 instructions, mais d'en émettre maximum 6 par cycle ! Et sur les CPU avec des fenêtres d'instruction décentralisées, il y a aussi une différence entre le nombre d'instructions qui sont ''dispatch'' et ''schedule''.
La catégorie la plus simple est celle des '''processeurs avec plusieurs pipelines'''. Ce sont des processeurs superscalaires sans exécution dans le désordre. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction.
Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, d'une FPU, éventuellement d'un second banc de registres, guère plus. Il permet d'exécuter une seconde opération de calcul, pas plus, ce qui est à l'origine de contraintes d'appariemment assez fortes. De plus, il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section.
===La double émission entière-flottante===
L'exemple le plus simple utilise un pipeline pour les opérations flottantes, séparé du pipeline originel. Pour bien expliquer les choses, prenons le processeur illustré ci-dessous. Un processeur a deux ''back-end'' séparés, un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par l'ALU entière, passent éventuellement dans l'unité mémoire, pour ensuite arriver à l'étape de ''writeback''. La FPU et le banc de registre flottant ont eux aussi leur propre ''back-end'' séparé.
[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]]
Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaqaue pipeline, à chaque cycle.
L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine.
Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants.
===La double émission entière===
Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait.
Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
==Les processeurs superscalaires dynamiques==
Les processeurs précédents, avec 2 pipelines, avaient une unité d'émission assez simple. Elle avait deux ports de décodage, et deux ports d'émission. Le nombre de ports de décodage et d'émission étaient identiques. Vous vous dites que c'est naturel, et intuitif, que c'est normal pour un processeur superscalaire. Mais en réalité, la majorité des processeurs superscalaires a plus de ports d'émission que de décodage. Expliquer pourquoi va cependant nous demander de parler de comment la superscalarité exploite les unités de calcul.
Posons-nous cette question : de combien d'unités un processeur superscalaire a besoin ? Les unités en question ne sont nécessairement des unité de calcul, l'unité mémoire et l'unité de branchement comptent aussi. La réponse dépend du nombre de voies. Si le ''front-end'' charge et décode un paquet de N instructions, exécuter ces N instructions simultanément demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section.
La réponse dépend aussi des contraintes d’appariement. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission parfaite devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement, ce qui ferait passer de 6 unités à 12. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, le cout en circuit encore plus prohibitif !
En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée, alors que la double émission parait plus adaptée.
Et c'est là que la différence entre ports d'émission et de décodage devient intéressante. L'idée est d'avoir une unité d'émission avec deux ports de décodage, et 4 port d'émission (un par unité). Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux micro-opérations tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu.
Les tout premiers CPU superscalaires étaient nombreux à utiliser cette technique. Les exemples classiques sont l'HyperSPARC, de l'alpha 21064 et du PowerPC 603. C'étaient des CPU double émission, avec une ALU entière, une unité mémoire et une FPU. Nous parlerons de '''partitionnement INT-FLOAT-MEM'''. Les contraintes d’appariement variaient grandement suivant le processeur.
Le PowerPC 603 implémentait une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur.
Avant de poursuivre, précisons que la FPU regroupe un additionneur flottant et un multiplieur flottant. Et ce fait peut être exploité pour approfondir le partitionnement. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions).
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple, facile à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. La raison est que le partitionnement permet d'émettre des paires, triplets, quadruplets de micro-opérations qui sont rares en pratique.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''.
Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, 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. Et enfin, il faut rajouter un port d'émission pour pouvoir émettre une micro-opération entière en plus.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle.
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
Reste à connecter les unités de calcul à l'unité d'émission (indirectement, car il y a des étages de pipeline entre les deux). Une implémentation basique utilisee un port d'émission par unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Et chaque unité a son propre port d'émission. La conséquence est que les contraintes d'appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La présence d'un circuit multiplieur et d'un ''barrel shifter'' pose des problèmes pour le partitionnement. Il est possible d'avoir un port d'émission dédié au multiplieur, et un autre au ''barrel shifter'', qui sont séparés de ceux pour l'ALU entière. Il est alors possible d'émettre une opération entière, une multiplication et un décalage, simultanément. Nous parlerons de '''partitionnement entier''' pour désigner cette possibilité.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
Le partitionnement entier est cependant très rare, car le gain en performance serait trop faible au regard des couts. Quelques processeurs historiques l'utilisaient, mais ce n'est plus le cas de nos jours. La raison est que les multiplications et les décalages sont assez rares dans le code, ce qui fait que le port d'émission associés sont sous-utilisés. Et un port d'émission a un cout en circuit assez important. 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.
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul. L'exemple classique est justement lié aux circuits multiplieur et ''barrel shifters''. L'idée est que le multiplieur et le ''barrel shifter'' sont mis sur le même port d'émission qu'une ALU entière.
Prenons par exemple le processeur à double émission entière illustré ci-contre. Un port d'émission est relié à une ALU entière seule, et l'autre port d'émission alimente la seconde ALU entière et le multiplieur. La conséquence est que le processeur peut émettre deux opérations entières, dont une multiplication. Si on avait utilisé un port d'émission dédié au multiplieur, il aurait été possible d'émettre deux opérations entières et une multiplication. On perd donc une voie, mais on gagne en flexibilité et on économise des circuits.
[[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]]
En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais le partage des ports d'émission peut appliquer à n'importe quelle unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. 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.
La solution est de partager des ports d'émission, pour passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple relie une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites. Le CPU peut émettre 6 opérations entières simultanément, grâce à 6 ALU entières. Les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires mélangent les trois techniques===
Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autre unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents.
* Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU.
* La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants.
* La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables.
* Le '''partage des ports d'émission''' réduit le cout en circuit du CPU, pour des pertes de performances mineures si bien fait.
De nos jours, le partitionnement est utilisé en complément de la duplication des ALU entières, de la duplication des autres unités, et du partage des ports d'émission. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. Le partage des ports d'émission a lui aussi mis un peu de temps pour s'imposer.
Les CPU superscalaires tendent à mettre le paquet niveau opérations arithmétiques, mais pas forcément au point de remplir la totalité de la largeur du processeur. La règle est que pour N voies, le processeur dispose d'au moins N/2 ALU entières. Et N/2 est une borne basse, la plupart des CPU x86 ont environ tournent plutôt autour des 2/3, voire des 3/4. Attention, cela ne signifie pas que le processeur émet systématiquement N/2 opération entières, il peut par exemple remplacer des opérations entières par des opérations flottantes.
En général, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. La conséquence est que les processeurs ont commencé à dupliquer leurs unités mémoire dès qu'ils ont atteint la quadruple émission, pour les CPU grand public. Par exemple, la microarchitecture ''Golden Cove'' d'Intel dispose de cinq unités de calcul, trois unités mémoire (deux spécialisées dans les écritures, trois dans les lectures). La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire. Notez que le ratio est équilibré entre opérations entières et mémoire, avec soit égalité, soit un avantage pour les ALU entières.
Une autre régularité est que la largeur totale du processeur peut être occupée par des opérations entières/mémoire. Pour le dire autrement, sur un processeur quadruple émission, il pourra émettre un mix de 4 instructions entière/mémoire, avec zéro opération flottante. Idem avec des processeurs triples ou pentuple émission, voire plus. Si le processeur émet une opération flottante, elle remplace une opération entière.
Prenons l'exemple des 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. C'était des processeurs quadruple émission, ce qui fait que la moitié de la largeur du processeur sert pour des opérations entières, et l'autre moitié pour les accès mémoire.
{|class="wikitable"
|-
! Port 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
|}
Les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon, sont un exemple moins représentatif. Ces microarchitectures utilisent le partage des ports d'émission au maximum. C'était des microarchitectures à triple émission, avec six ports d’émission. Il y avait trois ports d'émission pour les opérations flottantes, et trois ports d'émission pour les micro-opérations entières/mémoire. Il y avait trois ALU entières et trois unités mémoire, chaque ALU partageait un port d'émission avec une unité mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements.
{|class="wikitable"
|-
! Port 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 || ALU entière || FADD || FMUL || FPU (autres opérations)
|-
| Multiplieur || || || || ||
|-
| LOAD/STORE || LOAD/STORE || LOAD/STORE || || ||
|}
Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair.
Par exemple, la microarchitecture ''Golden Cove'' a deux ''barrel shifters''.
{|class="wikitable"
|-
! Port 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 !! Port d'émission n°7 !! Port d'émission n°8 !! Port d'émission n°9 !! Port d'émission n°10
|-
| ALU entière || ALU entière || ALU entière || ALU entière || ALU entière || LOAD || LOAD || LOAD || STORE || STORE
|-
| ''Barrel shifter'' || Multiplieur || Multiplieur || ''Barrel shifter'' || || || || || ||
|-
| Branchements || Diviseur || || Branchements || || || || || ||
|}
==L'évolution historique de la superscalarité==
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. La plupart des CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 sont rapidement montés à de la quadruple émission, avec quelques exceptions. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 ||
|-
! 1995
| 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000
|-
! 1996
| class="f_bleu | 620 || || || || class="f_bleu | 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
Les premiers CPU à double émission sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Voici le tableau chronologique disant quel CPU utilisait quelle technique.
* Les CPU en vert utilisaient la double émission entière-flottante.
* Les CPU en rouge utilisaient la double émission entière.
* Les CPU en jaune utilisaient un ''partitionnement complet'' entre ALU, FPU et unité mémoire.
* Les CPU sans couleur utilisaient un grand nombre d'unités, avec beaucoup de ports d'émission.
* Les CPU barrés utilisent la triple ou quadruple émission et seront vu dans la section suivante.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| class="f_vert | RS/6000 || || || class="f_rouge | i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| class="f_vert | RSC || class="f_vert | PA 7100 || <s>SuperSPARC</s> || || class="f_jaune | 21064 ||
|-
! 1993
| class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || ||
|-
! 1994
| <s>POWER 2</s> || PA 7200 || || || <s>21164</s> ||
|}
Le PA 7200 est un PA-7100 auquel on aurait rajouté une ALU entière. Le processeur exécutait les multiplications entières dans la FPU, comme son prédécesseur. Les combinaisons autorisées étaient donc INT + INT, INT + MEM, INT + FLOAT/MUL, MEM + FLOAT/MUL.
===Les processeurs historiques à quadruple émission===
C'est la même année , en 1994, que la quadruple émission est arrivée et que les CPU ont commencé à avoir de plus en plus d'unités de calcul. Même les designs restés en double émission avaient ajouté des unités de calcul. Il y avait alors trois stratégies pour la quadruple émission.
La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Les FPUs étaient soit dupliquées, soit scindées en deux en séparant l'additionneur du multiplieur flottant. Le POWER 2 dupliquait ses FPUs à la perfection, alors que le 21264 partitionnait la FPU. Les accès mémoire étaient traités comme des opérations entières, et le processeur pouvait : soit en faire un seul par cycle, soit en émettre deux, sous conditions (le cache devait être double port pour lire/écrire deux données à la fois, comme on le verra dans la suite du chapitre).
La seconde implémentation utilisait deux ALU entières, une unité mémoire et une FPU. De quoi exécuter en même temps deux opérations entières, une opération flottante, et un accès mémoire. La seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur). Un exemple est le SuperSPARC, un CPU triple émission. Il avait de nombreuses contraintes d’appariement. Par exemple, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Une bonne partie de ces contraintes était lié au banc de registre, qui n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions. Il faut noter que la seconde ALU a été abandonnée lors du passage au CPU HyperSPARC, qui est de plus repassé en double émission, ce qui est une évolution à rebours de l'évolution des autres CPUs.
Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, avec au minimum deux ALU entières. Pour comprendre ce que je veux dire, prenons l'exemple de l'UltraSPARC. Il avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplier flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes. Le processeur pouvait émettre 4 instructions, tant qu'elles atterrissaient dans des unités différentes. Le Motorola 88110, vu en exemple plus haut, était un CPU à double émission avec pas moins de 10 unités.
Une quatrième méthode dupliquait tout ce qui pouvait l'être. Les processeurs MIPS R8000 et R10000 avaient une implémentation plus moderne, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités. Moitié opérations entières, moitié opérations mémoire, avec de quoi remplacer les opérations entières par des opérations flottantes. Le PA 8000, lui, dupliquait toutes ses unités en deux exemplaires, ce qui n'est pas l'idéal, aucun processeur moderne ne fait cela aujourd'hui.
La morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes. Le tout est résumé dans le tableau ci-dessous, avec :
* en vert les CPU utilisant la première stratégie ;
* en rouge les CPU utilisant la seconde stratégie ;
* en jaune les CPU utilisant la troisième stratégie.
{|class="wikitable"
|+ Processeurs quadruple émission historiques
|-
! Année !! Processeur !! Unités
|-
! 1991
| class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques
|-
|-
! rowspan="2" | 1994
| class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'')
|-
| class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM
|-
! rowspan="3" | 1995
| PA 8000 || 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD
|-
| class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques
|-
| MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM
|-
! rowspan="2" | 1996
| class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM
|-
| class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL)
|}
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les CPU superscalaire à exécution dans le désordre==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Les unités d'exécution dans le désordre===
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports de ''dispatch''. Mais avec des stations de réservation, il faut le double des ports de ''schedule''. La raison est que les registres sont lus après émission avec une fenê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 de ''schedule'' que de ''dispatch''.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de 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===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Avec plusieurs dizaines d'unités de calcul différentes, ajouter autant 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.
La première solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Le processeur détecte quand il n'y a pas assez de ports pour servir toutes les micro-opérations. Quand ça arrive, l'unité d'émission émet assez de micro-opérations pour exploiter les ports disponibles, mais met en attente les micro-opérations en trop, le temps que les ports se libèrent. Pour cela, le banc de registre est géré par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Une autre solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une dernière solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
slmdvww4l3y2zlosaywu0jlcjqn6u9f
773209
773208
2026-09-25T21:12:58Z
Mewtow
31375
/* Les processeurs historiques à quadruple émission */
773209
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===L'impact des contraintes d’appariement sur la performance===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour la suite du chapitre===
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==Le ''front-end'' des processeurs superscalaires==
Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple.
Intuitivement, charger, décoder et 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 certaines processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. 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
|}
Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs.
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===L'émission parallèle des instructions===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elle sont propagée au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage).
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
==Les processeurs implémentés avec plusieurs pipelines==
Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Vous vous dites que l'implémentation d'un processeur superscalaire n'est pas la même suivant que le CPU utilise l'exécution dans le désordre, ou non. Mais dans les faits, la distinction est plus subtile que ca. Pour l'expliquer, je vais utiliser une classification originale, qui sépare les processeurs superscalaires en trois sous-types. La distinction se fait sur la manière dont la superscalarité est implémentée en matériel.La classification que j'utilise distingue :
* Les processeurs avec plusieurs pipelines, sans exécution dans le désordre.
* Les processeurs superscalaires avec un pipeline dynamique.
* Les processeurs avec des fenêtres d'instruction décentralisées.
La distinction n'est pas qu'une question d'implémentation matérielle. Elle impacte aussi le ratio entre instructions chargées/décodées et instructions émises. Vous vous dites que les deux sont égales, mais ce n'est le cas que sur les processeurs avec N pipelines ! Sur les autres processeurs, il est possible de décoder 4 instructions, mais d'en émettre maximum 6 par cycle ! Et sur les CPU avec des fenêtres d'instruction décentralisées, il y a aussi une différence entre le nombre d'instructions qui sont ''dispatch'' et ''schedule''.
La catégorie la plus simple est celle des '''processeurs avec plusieurs pipelines'''. Ce sont des processeurs superscalaires sans exécution dans le désordre. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction.
Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, d'une FPU, éventuellement d'un second banc de registres, guère plus. Il permet d'exécuter une seconde opération de calcul, pas plus, ce qui est à l'origine de contraintes d'appariemment assez fortes. De plus, il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section.
===La double émission entière-flottante===
L'exemple le plus simple utilise un pipeline pour les opérations flottantes, séparé du pipeline originel. Pour bien expliquer les choses, prenons le processeur illustré ci-dessous. Un processeur a deux ''back-end'' séparés, un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par l'ALU entière, passent éventuellement dans l'unité mémoire, pour ensuite arriver à l'étape de ''writeback''. La FPU et le banc de registre flottant ont eux aussi leur propre ''back-end'' séparé.
[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]]
Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaqaue pipeline, à chaque cycle.
L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine.
Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants.
===La double émission entière===
Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait.
Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
==Les processeurs superscalaires dynamiques==
Les processeurs précédents, avec 2 pipelines, avaient une unité d'émission assez simple. Elle avait deux ports de décodage, et deux ports d'émission. Le nombre de ports de décodage et d'émission étaient identiques. Vous vous dites que c'est naturel, et intuitif, que c'est normal pour un processeur superscalaire. Mais en réalité, la majorité des processeurs superscalaires a plus de ports d'émission que de décodage. Expliquer pourquoi va cependant nous demander de parler de comment la superscalarité exploite les unités de calcul.
Posons-nous cette question : de combien d'unités un processeur superscalaire a besoin ? Les unités en question ne sont nécessairement des unité de calcul, l'unité mémoire et l'unité de branchement comptent aussi. La réponse dépend du nombre de voies. Si le ''front-end'' charge et décode un paquet de N instructions, exécuter ces N instructions simultanément demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section.
La réponse dépend aussi des contraintes d’appariement. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission parfaite devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement, ce qui ferait passer de 6 unités à 12. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, le cout en circuit encore plus prohibitif !
En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée, alors que la double émission parait plus adaptée.
Et c'est là que la différence entre ports d'émission et de décodage devient intéressante. L'idée est d'avoir une unité d'émission avec deux ports de décodage, et 4 port d'émission (un par unité). Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux micro-opérations tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu.
Les tout premiers CPU superscalaires étaient nombreux à utiliser cette technique. Les exemples classiques sont l'HyperSPARC, de l'alpha 21064 et du PowerPC 603. C'étaient des CPU double émission, avec une ALU entière, une unité mémoire et une FPU. Nous parlerons de '''partitionnement INT-FLOAT-MEM'''. Les contraintes d’appariement variaient grandement suivant le processeur.
Le PowerPC 603 implémentait une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur.
Avant de poursuivre, précisons que la FPU regroupe un additionneur flottant et un multiplieur flottant. Et ce fait peut être exploité pour approfondir le partitionnement. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions).
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple, facile à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. La raison est que le partitionnement permet d'émettre des paires, triplets, quadruplets de micro-opérations qui sont rares en pratique.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''.
Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, 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. Et enfin, il faut rajouter un port d'émission pour pouvoir émettre une micro-opération entière en plus.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle.
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
Reste à connecter les unités de calcul à l'unité d'émission (indirectement, car il y a des étages de pipeline entre les deux). Une implémentation basique utilisee un port d'émission par unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Et chaque unité a son propre port d'émission. La conséquence est que les contraintes d'appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La présence d'un circuit multiplieur et d'un ''barrel shifter'' pose des problèmes pour le partitionnement. Il est possible d'avoir un port d'émission dédié au multiplieur, et un autre au ''barrel shifter'', qui sont séparés de ceux pour l'ALU entière. Il est alors possible d'émettre une opération entière, une multiplication et un décalage, simultanément. Nous parlerons de '''partitionnement entier''' pour désigner cette possibilité.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
Le partitionnement entier est cependant très rare, car le gain en performance serait trop faible au regard des couts. Quelques processeurs historiques l'utilisaient, mais ce n'est plus le cas de nos jours. La raison est que les multiplications et les décalages sont assez rares dans le code, ce qui fait que le port d'émission associés sont sous-utilisés. Et un port d'émission a un cout en circuit assez important. 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.
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul. L'exemple classique est justement lié aux circuits multiplieur et ''barrel shifters''. L'idée est que le multiplieur et le ''barrel shifter'' sont mis sur le même port d'émission qu'une ALU entière.
Prenons par exemple le processeur à double émission entière illustré ci-contre. Un port d'émission est relié à une ALU entière seule, et l'autre port d'émission alimente la seconde ALU entière et le multiplieur. La conséquence est que le processeur peut émettre deux opérations entières, dont une multiplication. Si on avait utilisé un port d'émission dédié au multiplieur, il aurait été possible d'émettre deux opérations entières et une multiplication. On perd donc une voie, mais on gagne en flexibilité et on économise des circuits.
[[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]]
En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais le partage des ports d'émission peut appliquer à n'importe quelle unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. 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.
La solution est de partager des ports d'émission, pour passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple relie une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites. Le CPU peut émettre 6 opérations entières simultanément, grâce à 6 ALU entières. Les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires mélangent les trois techniques===
Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autre unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents.
* Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU.
* La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants.
* La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables.
* Le '''partage des ports d'émission''' réduit le cout en circuit du CPU, pour des pertes de performances mineures si bien fait.
De nos jours, le partitionnement est utilisé en complément de la duplication des ALU entières, de la duplication des autres unités, et du partage des ports d'émission. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. Le partage des ports d'émission a lui aussi mis un peu de temps pour s'imposer.
Les CPU superscalaires tendent à mettre le paquet niveau opérations arithmétiques, mais pas forcément au point de remplir la totalité de la largeur du processeur. La règle est que pour N voies, le processeur dispose d'au moins N/2 ALU entières. Et N/2 est une borne basse, la plupart des CPU x86 ont environ tournent plutôt autour des 2/3, voire des 3/4. Attention, cela ne signifie pas que le processeur émet systématiquement N/2 opération entières, il peut par exemple remplacer des opérations entières par des opérations flottantes.
En général, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. La conséquence est que les processeurs ont commencé à dupliquer leurs unités mémoire dès qu'ils ont atteint la quadruple émission, pour les CPU grand public. Par exemple, la microarchitecture ''Golden Cove'' d'Intel dispose de cinq unités de calcul, trois unités mémoire (deux spécialisées dans les écritures, trois dans les lectures). La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire. Notez que le ratio est équilibré entre opérations entières et mémoire, avec soit égalité, soit un avantage pour les ALU entières.
Une autre régularité est que la largeur totale du processeur peut être occupée par des opérations entières/mémoire. Pour le dire autrement, sur un processeur quadruple émission, il pourra émettre un mix de 4 instructions entière/mémoire, avec zéro opération flottante. Idem avec des processeurs triples ou pentuple émission, voire plus. Si le processeur émet une opération flottante, elle remplace une opération entière.
Prenons l'exemple des 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. C'était des processeurs quadruple émission, ce qui fait que la moitié de la largeur du processeur sert pour des opérations entières, et l'autre moitié pour les accès mémoire.
{|class="wikitable"
|-
! Port 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
|}
Les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon, sont un exemple moins représentatif. Ces microarchitectures utilisent le partage des ports d'émission au maximum. C'était des microarchitectures à triple émission, avec six ports d’émission. Il y avait trois ports d'émission pour les opérations flottantes, et trois ports d'émission pour les micro-opérations entières/mémoire. Il y avait trois ALU entières et trois unités mémoire, chaque ALU partageait un port d'émission avec une unité mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements.
{|class="wikitable"
|-
! Port 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 || ALU entière || FADD || FMUL || FPU (autres opérations)
|-
| Multiplieur || || || || ||
|-
| LOAD/STORE || LOAD/STORE || LOAD/STORE || || ||
|}
Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair.
Par exemple, la microarchitecture ''Golden Cove'' a deux ''barrel shifters''.
{|class="wikitable"
|-
! Port 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 !! Port d'émission n°7 !! Port d'émission n°8 !! Port d'émission n°9 !! Port d'émission n°10
|-
| ALU entière || ALU entière || ALU entière || ALU entière || ALU entière || LOAD || LOAD || LOAD || STORE || STORE
|-
| ''Barrel shifter'' || Multiplieur || Multiplieur || ''Barrel shifter'' || || || || || ||
|-
| Branchements || Diviseur || || Branchements || || || || || ||
|}
==L'évolution historique de la superscalarité==
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. La plupart des CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 sont rapidement montés à de la quadruple émission, avec quelques exceptions. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 ||
|-
! 1995
| 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000
|-
! 1996
| class="f_bleu | 620 || || || || class="f_bleu | 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
Les premiers CPU à double émission sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Voici le tableau chronologique disant quel CPU utilisait quelle technique.
* Les CPU en vert utilisaient la double émission entière-flottante.
* Les CPU en rouge utilisaient la double émission entière.
* Les CPU en jaune utilisaient un ''partitionnement complet'' entre ALU, FPU et unité mémoire.
* Les CPU sans couleur utilisaient un grand nombre d'unités, avec beaucoup de ports d'émission.
* Les CPU barrés utilisent la triple ou quadruple émission et seront vu dans la section suivante.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| class="f_vert | RS/6000 || || || class="f_rouge | i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| class="f_vert | RSC || class="f_vert | PA 7100 || <s>SuperSPARC</s> || || class="f_jaune | 21064 ||
|-
! 1993
| class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || ||
|-
! 1994
| <s>POWER 2</s> || PA 7200 || || || <s>21164</s> ||
|}
Le PA 7200 est un PA-7100 auquel on aurait rajouté une ALU entière. Le processeur exécutait les multiplications entières dans la FPU, comme son prédécesseur. Les combinaisons autorisées étaient donc INT + INT, INT + MEM, INT + FLOAT/MUL, MEM + FLOAT/MUL.
C'est en 1994, que la quadruple émission est arrivée et que les CPU ont commencé à avoir de plus en plus d'unités de calcul. Même les designs restés en double émission avaient ajouté des unités de calcul. Il y avait alors trois stratégies pour la quadruple émission.
La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Les FPUs étaient soit dupliquées, soit scindées en deux en séparant l'additionneur du multiplieur flottant. Le POWER 2 dupliquait ses FPUs à la perfection, alors que le 21264 partitionnait la FPU. Les accès mémoire étaient traités comme des opérations entières, et le processeur pouvait : soit en faire un seul par cycle, soit en émettre deux, sous conditions (le cache devait être double port pour lire/écrire deux données à la fois, comme on le verra dans la suite du chapitre).
La seconde implémentation utilisait deux ALU entières, une unité mémoire et une FPU. De quoi exécuter en même temps deux opérations entières, une opération flottante, et un accès mémoire. La seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur). Un exemple est le SuperSPARC, un CPU triple émission. Il avait de nombreuses contraintes d’appariement. Par exemple, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Une bonne partie de ces contraintes était lié au banc de registre, qui n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions. Il faut noter que la seconde ALU a été abandonnée lors du passage au CPU HyperSPARC, qui est de plus repassé en double émission, ce qui est une évolution à rebours de l'évolution des autres CPUs.
Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, avec au minimum deux ALU entières. Pour comprendre ce que je veux dire, prenons l'exemple de l'UltraSPARC. Il avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplier flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes. Le processeur pouvait émettre 4 instructions, tant qu'elles atterrissaient dans des unités différentes. Le Motorola 88110, vu en exemple plus haut, était un CPU à double émission avec pas moins de 10 unités.
Une quatrième méthode dupliquait tout ce qui pouvait l'être. Les processeurs MIPS R8000 et R10000 avaient une implémentation plus moderne, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités. Moitié opérations entières, moitié opérations mémoire, avec de quoi remplacer les opérations entières par des opérations flottantes. Le PA 8000, lui, dupliquait toutes ses unités en deux exemplaires, ce qui n'est pas l'idéal, aucun processeur moderne ne fait cela aujourd'hui.
La morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes. Le tout est résumé dans le tableau ci-dessous, avec :
* en vert les CPU utilisant la première stratégie ;
* en rouge les CPU utilisant la seconde stratégie ;
* en jaune les CPU utilisant la troisième stratégie.
{|class="wikitable"
|+ Processeurs quadruple émission historiques
|-
! Année !! Processeur !! Unités
|-
! 1991
| class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques
|-
|-
! rowspan="2" | 1994
| class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'')
|-
| class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM
|-
! rowspan="3" | 1995
| PA 8000 || 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD
|-
| class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques
|-
| MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM
|-
! rowspan="2" | 1996
| class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM
|-
| class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL)
|}
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les CPU superscalaire à exécution dans le désordre==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Les unités d'exécution dans le désordre===
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports de ''dispatch''. Mais avec des stations de réservation, il faut le double des ports de ''schedule''. La raison est que les registres sont lus après émission avec une fenê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 de ''schedule'' que de ''dispatch''.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de 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===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Avec plusieurs dizaines d'unités de calcul différentes, ajouter autant 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.
La première solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Le processeur détecte quand il n'y a pas assez de ports pour servir toutes les micro-opérations. Quand ça arrive, l'unité d'émission émet assez de micro-opérations pour exploiter les ports disponibles, mais met en attente les micro-opérations en trop, le temps que les ports se libèrent. Pour cela, le banc de registre est géré par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Une autre solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une dernière solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
38z98vpxzl2qrms0q63bwz595ve2vkz
773210
773209
2026-09-25T21:14:38Z
Mewtow
31375
/* Les accès mémoire traités dans le pipeline entier */
773210
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===L'impact des contraintes d’appariement sur la performance===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour la suite du chapitre===
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==Le ''front-end'' des processeurs superscalaires==
Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple.
Intuitivement, charger, décoder et 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 certaines processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. 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
|}
Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs.
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===L'émission parallèle des instructions===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elle sont propagée au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage).
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
==Les processeurs implémentés avec plusieurs pipelines==
Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Vous vous dites que l'implémentation d'un processeur superscalaire n'est pas la même suivant que le CPU utilise l'exécution dans le désordre, ou non. Mais dans les faits, la distinction est plus subtile que ca. Pour l'expliquer, je vais utiliser une classification originale, qui sépare les processeurs superscalaires en trois sous-types. La distinction se fait sur la manière dont la superscalarité est implémentée en matériel.La classification que j'utilise distingue :
* Les processeurs avec plusieurs pipelines, sans exécution dans le désordre.
* Les processeurs superscalaires avec un pipeline dynamique.
* Les processeurs avec des fenêtres d'instruction décentralisées.
La distinction n'est pas qu'une question d'implémentation matérielle. Elle impacte aussi le ratio entre instructions chargées/décodées et instructions émises. Vous vous dites que les deux sont égales, mais ce n'est le cas que sur les processeurs avec N pipelines ! Sur les autres processeurs, il est possible de décoder 4 instructions, mais d'en émettre maximum 6 par cycle ! Et sur les CPU avec des fenêtres d'instruction décentralisées, il y a aussi une différence entre le nombre d'instructions qui sont ''dispatch'' et ''schedule''.
La catégorie la plus simple est celle des '''processeurs avec plusieurs pipelines'''. Ce sont des processeurs superscalaires sans exécution dans le désordre. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction.
Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, d'une FPU, éventuellement d'un second banc de registres, guère plus. Il permet d'exécuter une seconde opération de calcul, pas plus, ce qui est à l'origine de contraintes d'appariemment assez fortes. De plus, il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section.
===La double émission entière-flottante===
L'exemple le plus simple utilise un pipeline pour les opérations flottantes, séparé du pipeline originel. Pour bien expliquer les choses, prenons le processeur illustré ci-dessous. Un processeur a deux ''back-end'' séparés, un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par l'ALU entière, passent éventuellement dans l'unité mémoire, pour ensuite arriver à l'étape de ''writeback''. La FPU et le banc de registre flottant ont eux aussi leur propre ''back-end'' séparé.
[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]]
Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaqaue pipeline, à chaque cycle.
L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine.
Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants.
===La double émission entière===
Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait.
Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
==Les processeurs superscalaires dynamiques==
Les processeurs précédents, avec 2 pipelines, avaient une unité d'émission assez simple. Elle avait deux ports de décodage, et deux ports d'émission. Le nombre de ports de décodage et d'émission étaient identiques. Vous vous dites que c'est naturel, et intuitif, que c'est normal pour un processeur superscalaire. Mais en réalité, la majorité des processeurs superscalaires a plus de ports d'émission que de décodage. Expliquer pourquoi va cependant nous demander de parler de comment la superscalarité exploite les unités de calcul.
Posons-nous cette question : de combien d'unités un processeur superscalaire a besoin ? Les unités en question ne sont nécessairement des unité de calcul, l'unité mémoire et l'unité de branchement comptent aussi. La réponse dépend du nombre de voies. Si le ''front-end'' charge et décode un paquet de N instructions, exécuter ces N instructions simultanément demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section.
La réponse dépend aussi des contraintes d’appariement. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission parfaite devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement, ce qui ferait passer de 6 unités à 12. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, le cout en circuit encore plus prohibitif !
En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée, alors que la double émission parait plus adaptée.
Et c'est là que la différence entre ports d'émission et de décodage devient intéressante. L'idée est d'avoir une unité d'émission avec deux ports de décodage, et 4 port d'émission (un par unité). Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux micro-opérations tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu.
Les tout premiers CPU superscalaires étaient nombreux à utiliser cette technique. Les exemples classiques sont l'HyperSPARC, de l'alpha 21064 et du PowerPC 603. C'étaient des CPU double émission, avec une ALU entière, une unité mémoire et une FPU. Nous parlerons de '''partitionnement INT-FLOAT-MEM'''. Les contraintes d’appariement variaient grandement suivant le processeur.
Le PowerPC 603 implémentait une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur.
Avant de poursuivre, précisons que la FPU regroupe un additionneur flottant et un multiplieur flottant. Et ce fait peut être exploité pour approfondir le partitionnement. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions).
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple, facile à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. La raison est que le partitionnement permet d'émettre des paires, triplets, quadruplets de micro-opérations qui sont rares en pratique.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''.
Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, 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. Et enfin, il faut rajouter un port d'émission pour pouvoir émettre une micro-opération entière en plus.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle.
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
Reste à connecter les unités de calcul à l'unité d'émission (indirectement, car il y a des étages de pipeline entre les deux). Une implémentation basique utilisee un port d'émission par unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Et chaque unité a son propre port d'émission. La conséquence est que les contraintes d'appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La présence d'un circuit multiplieur et d'un ''barrel shifter'' pose des problèmes pour le partitionnement. Il est possible d'avoir un port d'émission dédié au multiplieur, et un autre au ''barrel shifter'', qui sont séparés de ceux pour l'ALU entière. Il est alors possible d'émettre une opération entière, une multiplication et un décalage, simultanément. Nous parlerons de '''partitionnement entier''' pour désigner cette possibilité.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
Le partitionnement entier est cependant très rare, car le gain en performance serait trop faible au regard des couts. Quelques processeurs historiques l'utilisaient, mais ce n'est plus le cas de nos jours. La raison est que les multiplications et les décalages sont assez rares dans le code, ce qui fait que le port d'émission associés sont sous-utilisés. Et un port d'émission a un cout en circuit assez important. 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.
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul. L'exemple classique est justement lié aux circuits multiplieur et ''barrel shifters''. L'idée est que le multiplieur et le ''barrel shifter'' sont mis sur le même port d'émission qu'une ALU entière.
Prenons par exemple le processeur à double émission entière illustré ci-contre. Un port d'émission est relié à une ALU entière seule, et l'autre port d'émission alimente la seconde ALU entière et le multiplieur. La conséquence est que le processeur peut émettre deux opérations entières, dont une multiplication. Si on avait utilisé un port d'émission dédié au multiplieur, il aurait été possible d'émettre deux opérations entières et une multiplication. On perd donc une voie, mais on gagne en flexibilité et on économise des circuits.
[[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]]
En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais le partage des ports d'émission peut appliquer à n'importe quelle unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. 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.
La solution est de partager des ports d'émission, pour passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple relie une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites. Le CPU peut émettre 6 opérations entières simultanément, grâce à 6 ALU entières. Les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires mélangent les trois techniques===
Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autre unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents.
* Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU.
* La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants.
* La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables.
* Le '''partage des ports d'émission''' réduit le cout en circuit du CPU, pour des pertes de performances mineures si bien fait.
De nos jours, le partitionnement est utilisé en complément de la duplication des ALU entières, de la duplication des autres unités, et du partage des ports d'émission. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. Le partage des ports d'émission a lui aussi mis un peu de temps pour s'imposer.
Les CPU superscalaires tendent à mettre le paquet niveau opérations arithmétiques, mais pas forcément au point de remplir la totalité de la largeur du processeur. La règle est que pour N voies, le processeur dispose d'au moins N/2 ALU entières. Et N/2 est une borne basse, la plupart des CPU x86 ont environ tournent plutôt autour des 2/3, voire des 3/4. Attention, cela ne signifie pas que le processeur émet systématiquement N/2 opération entières, il peut par exemple remplacer des opérations entières par des opérations flottantes.
En général, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. La conséquence est que les processeurs ont commencé à dupliquer leurs unités mémoire dès qu'ils ont atteint la quadruple émission, pour les CPU grand public. Par exemple, la microarchitecture ''Golden Cove'' d'Intel dispose de cinq unités de calcul, trois unités mémoire (deux spécialisées dans les écritures, trois dans les lectures). La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire. Notez que le ratio est équilibré entre opérations entières et mémoire, avec soit égalité, soit un avantage pour les ALU entières.
Une autre régularité est que la largeur totale du processeur peut être occupée par des opérations entières/mémoire. Pour le dire autrement, sur un processeur quadruple émission, il pourra émettre un mix de 4 instructions entière/mémoire, avec zéro opération flottante. Idem avec des processeurs triples ou pentuple émission, voire plus. Si le processeur émet une opération flottante, elle remplace une opération entière.
Prenons l'exemple des 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. C'était des processeurs quadruple émission, ce qui fait que la moitié de la largeur du processeur sert pour des opérations entières, et l'autre moitié pour les accès mémoire.
{|class="wikitable"
|-
! Port 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
|}
Les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon, sont un exemple moins représentatif. Ces microarchitectures utilisent le partage des ports d'émission au maximum. C'était des microarchitectures à triple émission, avec six ports d’émission. Il y avait trois ports d'émission pour les opérations flottantes, et trois ports d'émission pour les micro-opérations entières/mémoire. Il y avait trois ALU entières et trois unités mémoire, chaque ALU partageait un port d'émission avec une unité mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements.
{|class="wikitable"
|-
! Port 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 || ALU entière || FADD || FMUL || FPU (autres opérations)
|-
| Multiplieur || || || || ||
|-
| LOAD/STORE || LOAD/STORE || LOAD/STORE || || ||
|}
Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair.
Par exemple, la microarchitecture ''Golden Cove'' a deux ''barrel shifters''.
{|class="wikitable"
|-
! Port 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 !! Port d'émission n°7 !! Port d'émission n°8 !! Port d'émission n°9 !! Port d'émission n°10
|-
| ALU entière || ALU entière || ALU entière || ALU entière || ALU entière || LOAD || LOAD || LOAD || STORE || STORE
|-
| ''Barrel shifter'' || Multiplieur || Multiplieur || ''Barrel shifter'' || || || || || ||
|-
| Branchements || Diviseur || || Branchements || || || || || ||
|}
==L'évolution historique de la superscalarité==
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. La plupart des CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 sont rapidement montés à de la quadruple émission, avec quelques exceptions. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 ||
|-
! 1995
| 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000
|-
! 1996
| class="f_bleu | 620 || || || || class="f_bleu | 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
Les premiers CPU à double émission sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Voici le tableau chronologique disant quel CPU utilisait quelle technique.
* Les CPU en vert utilisaient la double émission entière-flottante.
* Les CPU en rouge utilisaient la double émission entière.
* Les CPU en jaune utilisaient un ''partitionnement complet'' entre ALU, FPU et unité mémoire.
* Les CPU sans couleur utilisaient un grand nombre d'unités, avec beaucoup de ports d'émission.
* Les CPU barrés utilisent la triple ou quadruple émission et seront vu dans la section suivante.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| class="f_vert | RS/6000 || || || class="f_rouge | i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| class="f_vert | RSC || class="f_vert | PA 7100 || <s>SuperSPARC</s> || || class="f_jaune | 21064 ||
|-
! 1993
| class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || ||
|-
! 1994
| <s>POWER 2</s> || PA 7200 || || || <s>21164</s> ||
|}
Le PA 7200 est un PA-7100 auquel on aurait rajouté une ALU entière. Le processeur exécutait les multiplications entières dans la FPU, comme son prédécesseur. Les combinaisons autorisées étaient donc INT + INT, INT + MEM, INT + FLOAT/MUL, MEM + FLOAT/MUL.
C'est en 1994, que la quadruple émission est arrivée et que les CPU ont commencé à avoir de plus en plus d'unités de calcul. Même les designs restés en double émission avaient ajouté des unités de calcul. Il y avait alors trois stratégies pour la quadruple émission.
La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Les FPUs étaient soit dupliquées, soit scindées en deux en séparant l'additionneur du multiplieur flottant. Le POWER 2 dupliquait ses FPUs à la perfection, alors que le 21264 partitionnait la FPU. Les accès mémoire étaient traités comme des opérations entières, et le processeur pouvait : soit en faire un seul par cycle, soit en émettre deux, sous conditions (le cache devait être double port pour lire/écrire deux données à la fois, comme on le verra dans la suite du chapitre).
La seconde implémentation utilisait deux ALU entières, une unité mémoire et une FPU. De quoi exécuter en même temps deux opérations entières, une opération flottante, et un accès mémoire. La seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur). Un exemple est le SuperSPARC, un CPU triple émission. Il avait de nombreuses contraintes d’appariement. Par exemple, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Une bonne partie de ces contraintes était lié au banc de registre, qui n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions. Il faut noter que la seconde ALU a été abandonnée lors du passage au CPU HyperSPARC, qui est de plus repassé en double émission, ce qui est une évolution à rebours de l'évolution des autres CPUs.
Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, avec au minimum deux ALU entières. Pour comprendre ce que je veux dire, prenons l'exemple de l'UltraSPARC. Il avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplier flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes. Le processeur pouvait émettre 4 instructions, tant qu'elles atterrissaient dans des unités différentes. Le Motorola 88110, vu en exemple plus haut, était un CPU à double émission avec pas moins de 10 unités.
Une quatrième méthode dupliquait tout ce qui pouvait l'être. Les processeurs MIPS R8000 et R10000 avaient une implémentation plus moderne, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités. Moitié opérations entières, moitié opérations mémoire, avec de quoi remplacer les opérations entières par des opérations flottantes. Le PA 8000, lui, dupliquait toutes ses unités en deux exemplaires, ce qui n'est pas l'idéal, aucun processeur moderne ne fait cela aujourd'hui.
La morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes. Le tout est résumé dans le tableau ci-dessous, avec :
* en vert les CPU utilisant la première stratégie ;
* en rouge les CPU utilisant la seconde stratégie ;
* en jaune les CPU utilisant la troisième stratégie.
{|class="wikitable"
|+ Processeurs quadruple émission historiques
|-
! Année !! Processeur !! Unités
|-
! 1991
| class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques
|-
|-
! rowspan="2" | 1994
| class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'')
|-
| class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM
|-
! rowspan="3" | 1995
| PA 8000 || 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD
|-
| class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques
|-
| MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM
|-
! rowspan="2" | 1996
| class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM
|-
| class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL)
|}
==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===
Dans le cas le plus simple, les accès mémoire sont traités comme des opérations entières un peu spéciales. Les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les CPU superscalaire à exécution dans le désordre==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Les unités d'exécution dans le désordre===
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports de ''dispatch''. Mais avec des stations de réservation, il faut le double des ports de ''schedule''. La raison est que les registres sont lus après émission avec une fenê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 de ''schedule'' que de ''dispatch''.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de 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===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Avec plusieurs dizaines d'unités de calcul différentes, ajouter autant 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.
La première solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Le processeur détecte quand il n'y a pas assez de ports pour servir toutes les micro-opérations. Quand ça arrive, l'unité d'émission émet assez de micro-opérations pour exploiter les ports disponibles, mais met en attente les micro-opérations en trop, le temps que les ports se libèrent. Pour cela, le banc de registre est géré par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Une autre solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une dernière solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
eyago3i96zyolidmshr5lnz6ycz5e1x
773211
773210
2026-09-25T21:15:15Z
Mewtow
31375
/* Les accès mémoire traités dans le pipeline entier */
773211
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===L'impact des contraintes d’appariement sur la performance===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour la suite du chapitre===
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==Le ''front-end'' des processeurs superscalaires==
Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple.
Intuitivement, charger, décoder et 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 certaines processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. 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
|}
Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs.
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===L'émission parallèle des instructions===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elle sont propagée au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage).
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
==Les processeurs implémentés avec plusieurs pipelines==
Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Vous vous dites que l'implémentation d'un processeur superscalaire n'est pas la même suivant que le CPU utilise l'exécution dans le désordre, ou non. Mais dans les faits, la distinction est plus subtile que ca. Pour l'expliquer, je vais utiliser une classification originale, qui sépare les processeurs superscalaires en trois sous-types. La distinction se fait sur la manière dont la superscalarité est implémentée en matériel.La classification que j'utilise distingue :
* Les processeurs avec plusieurs pipelines, sans exécution dans le désordre.
* Les processeurs superscalaires avec un pipeline dynamique.
* Les processeurs avec des fenêtres d'instruction décentralisées.
La distinction n'est pas qu'une question d'implémentation matérielle. Elle impacte aussi le ratio entre instructions chargées/décodées et instructions émises. Vous vous dites que les deux sont égales, mais ce n'est le cas que sur les processeurs avec N pipelines ! Sur les autres processeurs, il est possible de décoder 4 instructions, mais d'en émettre maximum 6 par cycle ! Et sur les CPU avec des fenêtres d'instruction décentralisées, il y a aussi une différence entre le nombre d'instructions qui sont ''dispatch'' et ''schedule''.
La catégorie la plus simple est celle des '''processeurs avec plusieurs pipelines'''. Ce sont des processeurs superscalaires sans exécution dans le désordre. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction.
Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, d'une FPU, éventuellement d'un second banc de registres, guère plus. Il permet d'exécuter une seconde opération de calcul, pas plus, ce qui est à l'origine de contraintes d'appariemment assez fortes. De plus, il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section.
===La double émission entière-flottante===
L'exemple le plus simple utilise un pipeline pour les opérations flottantes, séparé du pipeline originel. Pour bien expliquer les choses, prenons le processeur illustré ci-dessous. Un processeur a deux ''back-end'' séparés, un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par l'ALU entière, passent éventuellement dans l'unité mémoire, pour ensuite arriver à l'étape de ''writeback''. La FPU et le banc de registre flottant ont eux aussi leur propre ''back-end'' séparé.
[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]]
Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaqaue pipeline, à chaque cycle.
L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine.
Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants.
===La double émission entière===
Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait.
Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
==Les processeurs superscalaires dynamiques==
Les processeurs précédents, avec 2 pipelines, avaient une unité d'émission assez simple. Elle avait deux ports de décodage, et deux ports d'émission. Le nombre de ports de décodage et d'émission étaient identiques. Vous vous dites que c'est naturel, et intuitif, que c'est normal pour un processeur superscalaire. Mais en réalité, la majorité des processeurs superscalaires a plus de ports d'émission que de décodage. Expliquer pourquoi va cependant nous demander de parler de comment la superscalarité exploite les unités de calcul.
Posons-nous cette question : de combien d'unités un processeur superscalaire a besoin ? Les unités en question ne sont nécessairement des unité de calcul, l'unité mémoire et l'unité de branchement comptent aussi. La réponse dépend du nombre de voies. Si le ''front-end'' charge et décode un paquet de N instructions, exécuter ces N instructions simultanément demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section.
La réponse dépend aussi des contraintes d’appariement. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission parfaite devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement, ce qui ferait passer de 6 unités à 12. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, le cout en circuit encore plus prohibitif !
En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée, alors que la double émission parait plus adaptée.
Et c'est là que la différence entre ports d'émission et de décodage devient intéressante. L'idée est d'avoir une unité d'émission avec deux ports de décodage, et 4 port d'émission (un par unité). Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux micro-opérations tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu.
Les tout premiers CPU superscalaires étaient nombreux à utiliser cette technique. Les exemples classiques sont l'HyperSPARC, de l'alpha 21064 et du PowerPC 603. C'étaient des CPU double émission, avec une ALU entière, une unité mémoire et une FPU. Nous parlerons de '''partitionnement INT-FLOAT-MEM'''. Les contraintes d’appariement variaient grandement suivant le processeur.
Le PowerPC 603 implémentait une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur.
Avant de poursuivre, précisons que la FPU regroupe un additionneur flottant et un multiplieur flottant. Et ce fait peut être exploité pour approfondir le partitionnement. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions).
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple, facile à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. La raison est que le partitionnement permet d'émettre des paires, triplets, quadruplets de micro-opérations qui sont rares en pratique.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''.
Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, 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. Et enfin, il faut rajouter un port d'émission pour pouvoir émettre une micro-opération entière en plus.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle.
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
Reste à connecter les unités de calcul à l'unité d'émission (indirectement, car il y a des étages de pipeline entre les deux). Une implémentation basique utilisee un port d'émission par unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Et chaque unité a son propre port d'émission. La conséquence est que les contraintes d'appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La présence d'un circuit multiplieur et d'un ''barrel shifter'' pose des problèmes pour le partitionnement. Il est possible d'avoir un port d'émission dédié au multiplieur, et un autre au ''barrel shifter'', qui sont séparés de ceux pour l'ALU entière. Il est alors possible d'émettre une opération entière, une multiplication et un décalage, simultanément. Nous parlerons de '''partitionnement entier''' pour désigner cette possibilité.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
Le partitionnement entier est cependant très rare, car le gain en performance serait trop faible au regard des couts. Quelques processeurs historiques l'utilisaient, mais ce n'est plus le cas de nos jours. La raison est que les multiplications et les décalages sont assez rares dans le code, ce qui fait que le port d'émission associés sont sous-utilisés. Et un port d'émission a un cout en circuit assez important. 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.
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul. L'exemple classique est justement lié aux circuits multiplieur et ''barrel shifters''. L'idée est que le multiplieur et le ''barrel shifter'' sont mis sur le même port d'émission qu'une ALU entière.
Prenons par exemple le processeur à double émission entière illustré ci-contre. Un port d'émission est relié à une ALU entière seule, et l'autre port d'émission alimente la seconde ALU entière et le multiplieur. La conséquence est que le processeur peut émettre deux opérations entières, dont une multiplication. Si on avait utilisé un port d'émission dédié au multiplieur, il aurait été possible d'émettre deux opérations entières et une multiplication. On perd donc une voie, mais on gagne en flexibilité et on économise des circuits.
[[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]]
En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais le partage des ports d'émission peut appliquer à n'importe quelle unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. 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.
La solution est de partager des ports d'émission, pour passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple relie une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites. Le CPU peut émettre 6 opérations entières simultanément, grâce à 6 ALU entières. Les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires mélangent les trois techniques===
Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autre unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents.
* Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU.
* La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants.
* La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables.
* Le '''partage des ports d'émission''' réduit le cout en circuit du CPU, pour des pertes de performances mineures si bien fait.
De nos jours, le partitionnement est utilisé en complément de la duplication des ALU entières, de la duplication des autres unités, et du partage des ports d'émission. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. Le partage des ports d'émission a lui aussi mis un peu de temps pour s'imposer.
Les CPU superscalaires tendent à mettre le paquet niveau opérations arithmétiques, mais pas forcément au point de remplir la totalité de la largeur du processeur. La règle est que pour N voies, le processeur dispose d'au moins N/2 ALU entières. Et N/2 est une borne basse, la plupart des CPU x86 ont environ tournent plutôt autour des 2/3, voire des 3/4. Attention, cela ne signifie pas que le processeur émet systématiquement N/2 opération entières, il peut par exemple remplacer des opérations entières par des opérations flottantes.
En général, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. La conséquence est que les processeurs ont commencé à dupliquer leurs unités mémoire dès qu'ils ont atteint la quadruple émission, pour les CPU grand public. Par exemple, la microarchitecture ''Golden Cove'' d'Intel dispose de cinq unités de calcul, trois unités mémoire (deux spécialisées dans les écritures, trois dans les lectures). La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire. Notez que le ratio est équilibré entre opérations entières et mémoire, avec soit égalité, soit un avantage pour les ALU entières.
Une autre régularité est que la largeur totale du processeur peut être occupée par des opérations entières/mémoire. Pour le dire autrement, sur un processeur quadruple émission, il pourra émettre un mix de 4 instructions entière/mémoire, avec zéro opération flottante. Idem avec des processeurs triples ou pentuple émission, voire plus. Si le processeur émet une opération flottante, elle remplace une opération entière.
Prenons l'exemple des 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. C'était des processeurs quadruple émission, ce qui fait que la moitié de la largeur du processeur sert pour des opérations entières, et l'autre moitié pour les accès mémoire.
{|class="wikitable"
|-
! Port 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
|}
Les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon, sont un exemple moins représentatif. Ces microarchitectures utilisent le partage des ports d'émission au maximum. C'était des microarchitectures à triple émission, avec six ports d’émission. Il y avait trois ports d'émission pour les opérations flottantes, et trois ports d'émission pour les micro-opérations entières/mémoire. Il y avait trois ALU entières et trois unités mémoire, chaque ALU partageait un port d'émission avec une unité mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements.
{|class="wikitable"
|-
! Port 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 || ALU entière || FADD || FMUL || FPU (autres opérations)
|-
| Multiplieur || || || || ||
|-
| LOAD/STORE || LOAD/STORE || LOAD/STORE || || ||
|}
Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair.
Par exemple, la microarchitecture ''Golden Cove'' a deux ''barrel shifters''.
{|class="wikitable"
|-
! Port 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 !! Port d'émission n°7 !! Port d'émission n°8 !! Port d'émission n°9 !! Port d'émission n°10
|-
| ALU entière || ALU entière || ALU entière || ALU entière || ALU entière || LOAD || LOAD || LOAD || STORE || STORE
|-
| ''Barrel shifter'' || Multiplieur || Multiplieur || ''Barrel shifter'' || || || || || ||
|-
| Branchements || Diviseur || || Branchements || || || || || ||
|}
==L'évolution historique de la superscalarité==
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. La plupart des CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 sont rapidement montés à de la quadruple émission, avec quelques exceptions. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 ||
|-
! 1995
| 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000
|-
! 1996
| class="f_bleu | 620 || || || || class="f_bleu | 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
Les premiers CPU à double émission sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Voici le tableau chronologique disant quel CPU utilisait quelle technique.
* Les CPU en vert utilisaient la double émission entière-flottante.
* Les CPU en rouge utilisaient la double émission entière.
* Les CPU en jaune utilisaient un ''partitionnement complet'' entre ALU, FPU et unité mémoire.
* Les CPU sans couleur utilisaient un grand nombre d'unités, avec beaucoup de ports d'émission.
* Les CPU barrés utilisent la triple ou quadruple émission et seront vu dans la section suivante.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| class="f_vert | RS/6000 || || || class="f_rouge | i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| class="f_vert | RSC || class="f_vert | PA 7100 || <s>SuperSPARC</s> || || class="f_jaune | 21064 ||
|-
! 1993
| class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || ||
|-
! 1994
| <s>POWER 2</s> || PA 7200 || || || <s>21164</s> ||
|}
Le PA 7200 est un PA-7100 auquel on aurait rajouté une ALU entière. Le processeur exécutait les multiplications entières dans la FPU, comme son prédécesseur. Les combinaisons autorisées étaient donc INT + INT, INT + MEM, INT + FLOAT/MUL, MEM + FLOAT/MUL.
C'est en 1994, que la quadruple émission est arrivée et que les CPU ont commencé à avoir de plus en plus d'unités de calcul. Même les designs restés en double émission avaient ajouté des unités de calcul. Il y avait alors trois stratégies pour la quadruple émission.
La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Les FPUs étaient soit dupliquées, soit scindées en deux en séparant l'additionneur du multiplieur flottant. Le POWER 2 dupliquait ses FPUs à la perfection, alors que le 21264 partitionnait la FPU. Les accès mémoire étaient traités comme des opérations entières, et le processeur pouvait : soit en faire un seul par cycle, soit en émettre deux, sous conditions (le cache devait être double port pour lire/écrire deux données à la fois, comme on le verra dans la suite du chapitre).
La seconde implémentation utilisait deux ALU entières, une unité mémoire et une FPU. De quoi exécuter en même temps deux opérations entières, une opération flottante, et un accès mémoire. La seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur). Un exemple est le SuperSPARC, un CPU triple émission. Il avait de nombreuses contraintes d’appariement. Par exemple, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Une bonne partie de ces contraintes était lié au banc de registre, qui n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions. Il faut noter que la seconde ALU a été abandonnée lors du passage au CPU HyperSPARC, qui est de plus repassé en double émission, ce qui est une évolution à rebours de l'évolution des autres CPUs.
Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, avec au minimum deux ALU entières. Pour comprendre ce que je veux dire, prenons l'exemple de l'UltraSPARC. Il avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplier flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes. Le processeur pouvait émettre 4 instructions, tant qu'elles atterrissaient dans des unités différentes. Le Motorola 88110, vu en exemple plus haut, était un CPU à double émission avec pas moins de 10 unités.
Une quatrième méthode dupliquait tout ce qui pouvait l'être. Les processeurs MIPS R8000 et R10000 avaient une implémentation plus moderne, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités. Moitié opérations entières, moitié opérations mémoire, avec de quoi remplacer les opérations entières par des opérations flottantes. Le PA 8000, lui, dupliquait toutes ses unités en deux exemplaires, ce qui n'est pas l'idéal, aucun processeur moderne ne fait cela aujourd'hui.
La morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes. Le tout est résumé dans le tableau ci-dessous, avec :
* en vert les CPU utilisant la première stratégie ;
* en rouge les CPU utilisant la seconde stratégie ;
* en jaune les CPU utilisant la troisième stratégie.
{|class="wikitable"
|+ Processeurs quadruple émission historiques
|-
! Année !! Processeur !! Unités
|-
! 1991
| class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques
|-
|-
! rowspan="2" | 1994
| class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'')
|-
| class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM
|-
! rowspan="3" | 1995
| PA 8000 || 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD
|-
| class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques
|-
| MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM
|-
! rowspan="2" | 1996
| class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM
|-
| class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL)
|}
==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===
Dans le cas le plus simple, les accès mémoire sont traités comme des opérations entières un peu spéciales. L'unité mémoire est sur le même port d'émission que l'ALU entière, ce qui fait que les accès mémoire remplacent des opérations entières. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les CPU superscalaire à exécution dans le désordre==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Les unités d'exécution dans le désordre===
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports de ''dispatch''. Mais avec des stations de réservation, il faut le double des ports de ''schedule''. La raison est que les registres sont lus après émission avec une fenê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 de ''schedule'' que de ''dispatch''.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de 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===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Avec plusieurs dizaines d'unités de calcul différentes, ajouter autant 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.
La première solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Le processeur détecte quand il n'y a pas assez de ports pour servir toutes les micro-opérations. Quand ça arrive, l'unité d'émission émet assez de micro-opérations pour exploiter les ports disponibles, mais met en attente les micro-opérations en trop, le temps que les ports se libèrent. Pour cela, le banc de registre est géré par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Une autre solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une dernière solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
jcwuihek6ye9dh4fegvv6tb9admemff
773212
773211
2026-09-25T21:15:39Z
Mewtow
31375
/* L'évolution historique de la superscalarité */
773212
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===L'impact des contraintes d’appariement sur la performance===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour la suite du chapitre===
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==Le ''front-end'' des processeurs superscalaires==
Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple.
Intuitivement, charger, décoder et 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 certaines processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. 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
|}
Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs.
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===L'émission parallèle des instructions===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elle sont propagée au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage).
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
==Les processeurs implémentés avec plusieurs pipelines==
Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Vous vous dites que l'implémentation d'un processeur superscalaire n'est pas la même suivant que le CPU utilise l'exécution dans le désordre, ou non. Mais dans les faits, la distinction est plus subtile que ca. Pour l'expliquer, je vais utiliser une classification originale, qui sépare les processeurs superscalaires en trois sous-types. La distinction se fait sur la manière dont la superscalarité est implémentée en matériel.La classification que j'utilise distingue :
* Les processeurs avec plusieurs pipelines, sans exécution dans le désordre.
* Les processeurs superscalaires avec un pipeline dynamique.
* Les processeurs avec des fenêtres d'instruction décentralisées.
La distinction n'est pas qu'une question d'implémentation matérielle. Elle impacte aussi le ratio entre instructions chargées/décodées et instructions émises. Vous vous dites que les deux sont égales, mais ce n'est le cas que sur les processeurs avec N pipelines ! Sur les autres processeurs, il est possible de décoder 4 instructions, mais d'en émettre maximum 6 par cycle ! Et sur les CPU avec des fenêtres d'instruction décentralisées, il y a aussi une différence entre le nombre d'instructions qui sont ''dispatch'' et ''schedule''.
La catégorie la plus simple est celle des '''processeurs avec plusieurs pipelines'''. Ce sont des processeurs superscalaires sans exécution dans le désordre. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction.
Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, d'une FPU, éventuellement d'un second banc de registres, guère plus. Il permet d'exécuter une seconde opération de calcul, pas plus, ce qui est à l'origine de contraintes d'appariemment assez fortes. De plus, il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section.
===La double émission entière-flottante===
L'exemple le plus simple utilise un pipeline pour les opérations flottantes, séparé du pipeline originel. Pour bien expliquer les choses, prenons le processeur illustré ci-dessous. Un processeur a deux ''back-end'' séparés, un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par l'ALU entière, passent éventuellement dans l'unité mémoire, pour ensuite arriver à l'étape de ''writeback''. La FPU et le banc de registre flottant ont eux aussi leur propre ''back-end'' séparé.
[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]]
Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaqaue pipeline, à chaque cycle.
L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine.
Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants.
===La double émission entière===
Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait.
Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
==Les processeurs superscalaires dynamiques==
Les processeurs précédents, avec 2 pipelines, avaient une unité d'émission assez simple. Elle avait deux ports de décodage, et deux ports d'émission. Le nombre de ports de décodage et d'émission étaient identiques. Vous vous dites que c'est naturel, et intuitif, que c'est normal pour un processeur superscalaire. Mais en réalité, la majorité des processeurs superscalaires a plus de ports d'émission que de décodage. Expliquer pourquoi va cependant nous demander de parler de comment la superscalarité exploite les unités de calcul.
Posons-nous cette question : de combien d'unités un processeur superscalaire a besoin ? Les unités en question ne sont nécessairement des unité de calcul, l'unité mémoire et l'unité de branchement comptent aussi. La réponse dépend du nombre de voies. Si le ''front-end'' charge et décode un paquet de N instructions, exécuter ces N instructions simultanément demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section.
La réponse dépend aussi des contraintes d’appariement. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission parfaite devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement, ce qui ferait passer de 6 unités à 12. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, le cout en circuit encore plus prohibitif !
En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée, alors que la double émission parait plus adaptée.
Et c'est là que la différence entre ports d'émission et de décodage devient intéressante. L'idée est d'avoir une unité d'émission avec deux ports de décodage, et 4 port d'émission (un par unité). Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux micro-opérations tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu.
Les tout premiers CPU superscalaires étaient nombreux à utiliser cette technique. Les exemples classiques sont l'HyperSPARC, de l'alpha 21064 et du PowerPC 603. C'étaient des CPU double émission, avec une ALU entière, une unité mémoire et une FPU. Nous parlerons de '''partitionnement INT-FLOAT-MEM'''. Les contraintes d’appariement variaient grandement suivant le processeur.
Le PowerPC 603 implémentait une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur.
Avant de poursuivre, précisons que la FPU regroupe un additionneur flottant et un multiplieur flottant. Et ce fait peut être exploité pour approfondir le partitionnement. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions).
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple, facile à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. La raison est que le partitionnement permet d'émettre des paires, triplets, quadruplets de micro-opérations qui sont rares en pratique.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''.
Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, 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. Et enfin, il faut rajouter un port d'émission pour pouvoir émettre une micro-opération entière en plus.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle.
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
Reste à connecter les unités de calcul à l'unité d'émission (indirectement, car il y a des étages de pipeline entre les deux). Une implémentation basique utilisee un port d'émission par unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Et chaque unité a son propre port d'émission. La conséquence est que les contraintes d'appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La présence d'un circuit multiplieur et d'un ''barrel shifter'' pose des problèmes pour le partitionnement. Il est possible d'avoir un port d'émission dédié au multiplieur, et un autre au ''barrel shifter'', qui sont séparés de ceux pour l'ALU entière. Il est alors possible d'émettre une opération entière, une multiplication et un décalage, simultanément. Nous parlerons de '''partitionnement entier''' pour désigner cette possibilité.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
Le partitionnement entier est cependant très rare, car le gain en performance serait trop faible au regard des couts. Quelques processeurs historiques l'utilisaient, mais ce n'est plus le cas de nos jours. La raison est que les multiplications et les décalages sont assez rares dans le code, ce qui fait que le port d'émission associés sont sous-utilisés. Et un port d'émission a un cout en circuit assez important. 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.
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul. L'exemple classique est justement lié aux circuits multiplieur et ''barrel shifters''. L'idée est que le multiplieur et le ''barrel shifter'' sont mis sur le même port d'émission qu'une ALU entière.
Prenons par exemple le processeur à double émission entière illustré ci-contre. Un port d'émission est relié à une ALU entière seule, et l'autre port d'émission alimente la seconde ALU entière et le multiplieur. La conséquence est que le processeur peut émettre deux opérations entières, dont une multiplication. Si on avait utilisé un port d'émission dédié au multiplieur, il aurait été possible d'émettre deux opérations entières et une multiplication. On perd donc une voie, mais on gagne en flexibilité et on économise des circuits.
[[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]]
En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais le partage des ports d'émission peut appliquer à n'importe quelle unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. 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.
La solution est de partager des ports d'émission, pour passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple relie une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites. Le CPU peut émettre 6 opérations entières simultanément, grâce à 6 ALU entières. Les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires mélangent les trois techniques===
Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autre unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents.
* Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU.
* La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants.
* La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables.
* Le '''partage des ports d'émission''' réduit le cout en circuit du CPU, pour des pertes de performances mineures si bien fait.
De nos jours, le partitionnement est utilisé en complément de la duplication des ALU entières, de la duplication des autres unités, et du partage des ports d'émission. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. Le partage des ports d'émission a lui aussi mis un peu de temps pour s'imposer.
Les CPU superscalaires tendent à mettre le paquet niveau opérations arithmétiques, mais pas forcément au point de remplir la totalité de la largeur du processeur. La règle est que pour N voies, le processeur dispose d'au moins N/2 ALU entières. Et N/2 est une borne basse, la plupart des CPU x86 ont environ tournent plutôt autour des 2/3, voire des 3/4. Attention, cela ne signifie pas que le processeur émet systématiquement N/2 opération entières, il peut par exemple remplacer des opérations entières par des opérations flottantes.
En général, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. La conséquence est que les processeurs ont commencé à dupliquer leurs unités mémoire dès qu'ils ont atteint la quadruple émission, pour les CPU grand public. Par exemple, la microarchitecture ''Golden Cove'' d'Intel dispose de cinq unités de calcul, trois unités mémoire (deux spécialisées dans les écritures, trois dans les lectures). La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire. Notez que le ratio est équilibré entre opérations entières et mémoire, avec soit égalité, soit un avantage pour les ALU entières.
Une autre régularité est que la largeur totale du processeur peut être occupée par des opérations entières/mémoire. Pour le dire autrement, sur un processeur quadruple émission, il pourra émettre un mix de 4 instructions entière/mémoire, avec zéro opération flottante. Idem avec des processeurs triples ou pentuple émission, voire plus. Si le processeur émet une opération flottante, elle remplace une opération entière.
Prenons l'exemple des 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. C'était des processeurs quadruple émission, ce qui fait que la moitié de la largeur du processeur sert pour des opérations entières, et l'autre moitié pour les accès mémoire.
{|class="wikitable"
|-
! Port 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
|}
Les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon, sont un exemple moins représentatif. Ces microarchitectures utilisent le partage des ports d'émission au maximum. C'était des microarchitectures à triple émission, avec six ports d’émission. Il y avait trois ports d'émission pour les opérations flottantes, et trois ports d'émission pour les micro-opérations entières/mémoire. Il y avait trois ALU entières et trois unités mémoire, chaque ALU partageait un port d'émission avec une unité mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements.
{|class="wikitable"
|-
! Port 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 || ALU entière || FADD || FMUL || FPU (autres opérations)
|-
| Multiplieur || || || || ||
|-
| LOAD/STORE || LOAD/STORE || LOAD/STORE || || ||
|}
Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair.
Par exemple, la microarchitecture ''Golden Cove'' a deux ''barrel shifters''.
{|class="wikitable"
|-
! Port 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 !! Port d'émission n°7 !! Port d'émission n°8 !! Port d'émission n°9 !! Port d'émission n°10
|-
| ALU entière || ALU entière || ALU entière || ALU entière || ALU entière || LOAD || LOAD || LOAD || STORE || STORE
|-
| ''Barrel shifter'' || Multiplieur || Multiplieur || ''Barrel shifter'' || || || || || ||
|-
| Branchements || Diviseur || || Branchements || || || || || ||
|}
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. La plupart des CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 sont rapidement montés à de la quadruple émission, avec quelques exceptions. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 ||
|-
! 1995
| 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000
|-
! 1996
| class="f_bleu | 620 || || || || class="f_bleu | 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
Les premiers CPU à double émission sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Voici le tableau chronologique disant quel CPU utilisait quelle technique.
* Les CPU en vert utilisaient la double émission entière-flottante.
* Les CPU en rouge utilisaient la double émission entière.
* Les CPU en jaune utilisaient un ''partitionnement complet'' entre ALU, FPU et unité mémoire.
* Les CPU sans couleur utilisaient un grand nombre d'unités, avec beaucoup de ports d'émission.
* Les CPU barrés utilisent la triple ou quadruple émission et seront vu dans la section suivante.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| class="f_vert | RS/6000 || || || class="f_rouge | i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| class="f_vert | RSC || class="f_vert | PA 7100 || <s>SuperSPARC</s> || || class="f_jaune | 21064 ||
|-
! 1993
| class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || ||
|-
! 1994
| <s>POWER 2</s> || PA 7200 || || || <s>21164</s> ||
|}
Le PA 7200 est un PA-7100 auquel on aurait rajouté une ALU entière. Le processeur exécutait les multiplications entières dans la FPU, comme son prédécesseur. Les combinaisons autorisées étaient donc INT + INT, INT + MEM, INT + FLOAT/MUL, MEM + FLOAT/MUL.
C'est en 1994, que la quadruple émission est arrivée et que les CPU ont commencé à avoir de plus en plus d'unités de calcul. Même les designs restés en double émission avaient ajouté des unités de calcul. Il y avait alors trois stratégies pour la quadruple émission.
La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Les FPUs étaient soit dupliquées, soit scindées en deux en séparant l'additionneur du multiplieur flottant. Le POWER 2 dupliquait ses FPUs à la perfection, alors que le 21264 partitionnait la FPU. Les accès mémoire étaient traités comme des opérations entières, et le processeur pouvait : soit en faire un seul par cycle, soit en émettre deux, sous conditions (le cache devait être double port pour lire/écrire deux données à la fois, comme on le verra dans la suite du chapitre).
La seconde implémentation utilisait deux ALU entières, une unité mémoire et une FPU. De quoi exécuter en même temps deux opérations entières, une opération flottante, et un accès mémoire. La seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur). Un exemple est le SuperSPARC, un CPU triple émission. Il avait de nombreuses contraintes d’appariement. Par exemple, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Une bonne partie de ces contraintes était lié au banc de registre, qui n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions. Il faut noter que la seconde ALU a été abandonnée lors du passage au CPU HyperSPARC, qui est de plus repassé en double émission, ce qui est une évolution à rebours de l'évolution des autres CPUs.
Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, avec au minimum deux ALU entières. Pour comprendre ce que je veux dire, prenons l'exemple de l'UltraSPARC. Il avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplier flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes. Le processeur pouvait émettre 4 instructions, tant qu'elles atterrissaient dans des unités différentes. Le Motorola 88110, vu en exemple plus haut, était un CPU à double émission avec pas moins de 10 unités.
Une quatrième méthode dupliquait tout ce qui pouvait l'être. Les processeurs MIPS R8000 et R10000 avaient une implémentation plus moderne, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités. Moitié opérations entières, moitié opérations mémoire, avec de quoi remplacer les opérations entières par des opérations flottantes. Le PA 8000, lui, dupliquait toutes ses unités en deux exemplaires, ce qui n'est pas l'idéal, aucun processeur moderne ne fait cela aujourd'hui.
La morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes. Le tout est résumé dans le tableau ci-dessous, avec :
* en vert les CPU utilisant la première stratégie ;
* en rouge les CPU utilisant la seconde stratégie ;
* en jaune les CPU utilisant la troisième stratégie.
{|class="wikitable"
|+ Processeurs quadruple émission historiques
|-
! Année !! Processeur !! Unités
|-
! 1991
| class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques
|-
|-
! rowspan="2" | 1994
| class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'')
|-
| class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM
|-
! rowspan="3" | 1995
| PA 8000 || 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD
|-
| class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques
|-
| MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM
|-
! rowspan="2" | 1996
| class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM
|-
| class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL)
|}
==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===
Dans le cas le plus simple, les accès mémoire sont traités comme des opérations entières un peu spéciales. L'unité mémoire est sur le même port d'émission que l'ALU entière, ce qui fait que les accès mémoire remplacent des opérations entières. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les CPU superscalaire à exécution dans le désordre==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Les unités d'exécution dans le désordre===
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports de ''dispatch''. Mais avec des stations de réservation, il faut le double des ports de ''schedule''. La raison est que les registres sont lus après émission avec une fenê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 de ''schedule'' que de ''dispatch''.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de 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===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Avec plusieurs dizaines d'unités de calcul différentes, ajouter autant 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.
La première solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Le processeur détecte quand il n'y a pas assez de ports pour servir toutes les micro-opérations. Quand ça arrive, l'unité d'émission émet assez de micro-opérations pour exploiter les ports disponibles, mais met en attente les micro-opérations en trop, le temps que les ports se libèrent. Pour cela, le banc de registre est géré par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Une autre solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une dernière solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
tnjc7ejfalzqovcm90sgmg57vck97d3
773213
773212
2026-09-25T21:19:35Z
Mewtow
31375
/* Les CPU superscalaires mélangent les trois techniques */
773213
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===L'impact des contraintes d’appariement sur la performance===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour la suite du chapitre===
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==Le ''front-end'' des processeurs superscalaires==
Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple.
Intuitivement, charger, décoder et 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 certaines processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. 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
|}
Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs.
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===L'émission parallèle des instructions===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elle sont propagée au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage).
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
==Les processeurs implémentés avec plusieurs pipelines==
Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Vous vous dites que l'implémentation d'un processeur superscalaire n'est pas la même suivant que le CPU utilise l'exécution dans le désordre, ou non. Mais dans les faits, la distinction est plus subtile que ca. Pour l'expliquer, je vais utiliser une classification originale, qui sépare les processeurs superscalaires en trois sous-types. La distinction se fait sur la manière dont la superscalarité est implémentée en matériel.La classification que j'utilise distingue :
* Les processeurs avec plusieurs pipelines, sans exécution dans le désordre.
* Les processeurs superscalaires avec un pipeline dynamique.
* Les processeurs avec des fenêtres d'instruction décentralisées.
La distinction n'est pas qu'une question d'implémentation matérielle. Elle impacte aussi le ratio entre instructions chargées/décodées et instructions émises. Vous vous dites que les deux sont égales, mais ce n'est le cas que sur les processeurs avec N pipelines ! Sur les autres processeurs, il est possible de décoder 4 instructions, mais d'en émettre maximum 6 par cycle ! Et sur les CPU avec des fenêtres d'instruction décentralisées, il y a aussi une différence entre le nombre d'instructions qui sont ''dispatch'' et ''schedule''.
La catégorie la plus simple est celle des '''processeurs avec plusieurs pipelines'''. Ce sont des processeurs superscalaires sans exécution dans le désordre. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction.
Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, d'une FPU, éventuellement d'un second banc de registres, guère plus. Il permet d'exécuter une seconde opération de calcul, pas plus, ce qui est à l'origine de contraintes d'appariemment assez fortes. De plus, il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section.
===La double émission entière-flottante===
L'exemple le plus simple utilise un pipeline pour les opérations flottantes, séparé du pipeline originel. Pour bien expliquer les choses, prenons le processeur illustré ci-dessous. Un processeur a deux ''back-end'' séparés, un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par l'ALU entière, passent éventuellement dans l'unité mémoire, pour ensuite arriver à l'étape de ''writeback''. La FPU et le banc de registre flottant ont eux aussi leur propre ''back-end'' séparé.
[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]]
Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaqaue pipeline, à chaque cycle.
L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine.
Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants.
===La double émission entière===
Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait.
Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
==Les processeurs superscalaires dynamiques==
Les processeurs précédents, avec 2 pipelines, avaient une unité d'émission assez simple. Elle avait deux ports de décodage, et deux ports d'émission. Le nombre de ports de décodage et d'émission étaient identiques. Vous vous dites que c'est naturel, et intuitif, que c'est normal pour un processeur superscalaire. Mais en réalité, la majorité des processeurs superscalaires a plus de ports d'émission que de décodage. Expliquer pourquoi va cependant nous demander de parler de comment la superscalarité exploite les unités de calcul.
Posons-nous cette question : de combien d'unités un processeur superscalaire a besoin ? Les unités en question ne sont nécessairement des unité de calcul, l'unité mémoire et l'unité de branchement comptent aussi. La réponse dépend du nombre de voies. Si le ''front-end'' charge et décode un paquet de N instructions, exécuter ces N instructions simultanément demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section.
La réponse dépend aussi des contraintes d’appariement. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission parfaite devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement, ce qui ferait passer de 6 unités à 12. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, le cout en circuit encore plus prohibitif !
En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée, alors que la double émission parait plus adaptée.
Et c'est là que la différence entre ports d'émission et de décodage devient intéressante. L'idée est d'avoir une unité d'émission avec deux ports de décodage, et 4 port d'émission (un par unité). Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux micro-opérations tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu.
Les tout premiers CPU superscalaires étaient nombreux à utiliser cette technique. Les exemples classiques sont l'HyperSPARC, de l'alpha 21064 et du PowerPC 603. C'étaient des CPU double émission, avec une ALU entière, une unité mémoire et une FPU. Nous parlerons de '''partitionnement INT-FLOAT-MEM'''. Les contraintes d’appariement variaient grandement suivant le processeur.
Le PowerPC 603 implémentait une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur.
Avant de poursuivre, précisons que la FPU regroupe un additionneur flottant et un multiplieur flottant. Et ce fait peut être exploité pour approfondir le partitionnement. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions).
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple, facile à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. La raison est que le partitionnement permet d'émettre des paires, triplets, quadruplets de micro-opérations qui sont rares en pratique.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''.
Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, 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. Et enfin, il faut rajouter un port d'émission pour pouvoir émettre une micro-opération entière en plus.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle.
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
Reste à connecter les unités de calcul à l'unité d'émission (indirectement, car il y a des étages de pipeline entre les deux). Une implémentation basique utilisee un port d'émission par unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Et chaque unité a son propre port d'émission. La conséquence est que les contraintes d'appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La présence d'un circuit multiplieur et d'un ''barrel shifter'' pose des problèmes pour le partitionnement. Il est possible d'avoir un port d'émission dédié au multiplieur, et un autre au ''barrel shifter'', qui sont séparés de ceux pour l'ALU entière. Il est alors possible d'émettre une opération entière, une multiplication et un décalage, simultanément. Nous parlerons de '''partitionnement entier''' pour désigner cette possibilité.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
Le partitionnement entier est cependant très rare, car le gain en performance serait trop faible au regard des couts. Quelques processeurs historiques l'utilisaient, mais ce n'est plus le cas de nos jours. La raison est que les multiplications et les décalages sont assez rares dans le code, ce qui fait que le port d'émission associés sont sous-utilisés. Et un port d'émission a un cout en circuit assez important. 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.
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul. L'exemple classique est justement lié aux circuits multiplieur et ''barrel shifters''. L'idée est que le multiplieur et le ''barrel shifter'' sont mis sur le même port d'émission qu'une ALU entière.
Prenons par exemple le processeur à double émission entière illustré ci-contre. Un port d'émission est relié à une ALU entière seule, et l'autre port d'émission alimente la seconde ALU entière et le multiplieur. La conséquence est que le processeur peut émettre deux opérations entières, dont une multiplication. Si on avait utilisé un port d'émission dédié au multiplieur, il aurait été possible d'émettre deux opérations entières et une multiplication. On perd donc une voie, mais on gagne en flexibilité et on économise des circuits.
[[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]]
En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais le partage des ports d'émission peut appliquer à n'importe quelle unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. 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.
La solution est de partager des ports d'émission, pour passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple relie une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites. Le CPU peut émettre 6 opérations entières simultanément, grâce à 6 ALU entières. Les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires mélangent les trois techniques===
Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autre unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents.
* Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU.
* La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants.
* La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables.
* Le '''partage des ports d'émission''' réduit le cout en circuit du CPU, pour des pertes de performances mineures si bien fait.
L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. Le partage des ports d'émission a lui aussi mis un peu de temps pour s'imposer. Globalement, les processeurs superscalaires ont évolués progressivement, pour se rapprocher d'une sorte de standard. Mais pour comprendre ce qu'est ce standard, et en quoi les CPU historiques s'en sont progressivement rapprochés, il faut étudier l'histoire des CPU superscalaires.
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais abandonnés avant la production, comme l'ACS-1 d'IBM. Les années 1980 ont vu l'arrivée de prototypes de processeurs VLIW, mais qui n'étaient pas superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail.
Les premiers CPU superscalaires commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. Les CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 hésitaient entre double et quadruple émission. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 ||
|-
! 1995
| 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000
|-
! 1996
| class="f_bleu | 620 || || || || class="f_bleu | 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
Les premiers CPU à double émission sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Voici le tableau chronologique disant quel CPU utilisait quelle technique.
* Les CPU en vert utilisaient la double émission entière-flottante.
* Les CPU en rouge utilisaient la double émission entière.
* Les CPU en jaune utilisaient un ''partitionnement complet'' entre ALU, FPU et unité mémoire.
* Les CPU sans couleur utilisaient un grand nombre d'unités, avec beaucoup de ports d'émission.
* Les CPU barrés utilisent la triple ou quadruple émission et seront vu dans la section suivante.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| class="f_vert | RS/6000 || || || class="f_rouge | i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| class="f_vert | RSC || class="f_vert | PA 7100 || <s>SuperSPARC</s> || || class="f_jaune | 21064 ||
|-
! 1993
| class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || ||
|-
! 1994
| <s>POWER 2</s> || PA 7200 || || || <s>21164</s> ||
|}
Le PA 7200 est un PA-7100 auquel on aurait rajouté une ALU entière. Le processeur exécutait les multiplications entières dans la FPU, comme son prédécesseur. Les combinaisons autorisées étaient donc INT + INT, INT + MEM, INT + FLOAT/MUL, MEM + FLOAT/MUL.
C'est en 1994, que la quadruple émission est arrivée et que les CPU ont commencé à avoir de plus en plus d'unités de calcul. Même les designs restés en double émission avaient ajouté des unités de calcul. Il y avait alors trois stratégies pour la quadruple émission.
La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Les FPUs étaient soit dupliquées, soit scindées en deux en séparant l'additionneur du multiplieur flottant. Le POWER 2 dupliquait ses FPUs à la perfection, alors que le 21264 partitionnait la FPU. Les accès mémoire étaient traités comme des opérations entières, et le processeur pouvait : soit en faire un seul par cycle, soit en émettre deux, sous conditions (le cache devait être double port pour lire/écrire deux données à la fois, comme on le verra dans la suite du chapitre).
La seconde implémentation utilisait deux ALU entières, une unité mémoire et une FPU. De quoi exécuter en même temps deux opérations entières, une opération flottante, et un accès mémoire. La seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur). Un exemple est le SuperSPARC, un CPU triple émission. Il avait de nombreuses contraintes d’appariement. Par exemple, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Une bonne partie de ces contraintes était lié au banc de registre, qui n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions. Il faut noter que la seconde ALU a été abandonnée lors du passage au CPU HyperSPARC, qui est de plus repassé en double émission, ce qui est une évolution à rebours de l'évolution des autres CPUs.
Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, avec au minimum deux ALU entières. Pour comprendre ce que je veux dire, prenons l'exemple de l'UltraSPARC. Il avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplier flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes. Le processeur pouvait émettre 4 instructions, tant qu'elles atterrissaient dans des unités différentes. Le Motorola 88110, vu en exemple plus haut, était un CPU à double émission avec pas moins de 10 unités.
Une quatrième méthode dupliquait tout ce qui pouvait l'être. Les processeurs MIPS R8000 et R10000 avaient une implémentation plus moderne, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités. Moitié opérations entières, moitié opérations mémoire, avec de quoi remplacer les opérations entières par des opérations flottantes. Le PA 8000, lui, dupliquait toutes ses unités en deux exemplaires, ce qui n'est pas l'idéal, aucun processeur moderne ne fait cela aujourd'hui.
La morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes. Le tout est résumé dans le tableau ci-dessous, avec :
* en vert les CPU utilisant la première stratégie ;
* en rouge les CPU utilisant la seconde stratégie ;
* en jaune les CPU utilisant la troisième stratégie.
{|class="wikitable"
|+ Processeurs quadruple émission historiques
|-
! Année !! Processeur !! Unités
|-
! 1991
| class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques
|-
|-
! rowspan="2" | 1994
| class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'')
|-
| class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM
|-
! rowspan="3" | 1995
| PA 8000 || 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD
|-
| class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques
|-
| MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM
|-
! rowspan="2" | 1996
| class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM
|-
| class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL)
|}
Les CPU superscalaires tendent à mettre le paquet niveau opérations arithmétiques, mais pas forcément au point de remplir la totalité de la largeur du processeur. La règle est que pour N voies, le processeur dispose d'au moins N/2 ALU entières. Et N/2 est une borne basse, la plupart des CPU x86 ont environ tournent plutôt autour des 2/3, voire des 3/4. Attention, cela ne signifie pas que le processeur émet systématiquement N/2 opération entières, il peut par exemple remplacer des opérations entières par des opérations flottantes.
En général, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. La conséquence est que les processeurs ont commencé à dupliquer leurs unités mémoire dès qu'ils ont atteint la quadruple émission, pour les CPU grand public. Par exemple, la microarchitecture ''Golden Cove'' d'Intel dispose de cinq unités de calcul, trois unités mémoire (deux spécialisées dans les écritures, trois dans les lectures). La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire. Notez que le ratio est équilibré entre opérations entières et mémoire, avec soit égalité, soit un avantage pour les ALU entières.
Une autre régularité est que la largeur totale du processeur peut être occupée par des opérations entières/mémoire. Pour le dire autrement, sur un processeur quadruple émission, il pourra émettre un mix de 4 instructions entière/mémoire, avec zéro opération flottante. Idem avec des processeurs triples ou pentuple émission, voire plus. Si le processeur émet une opération flottante, elle remplace une opération entière.
Prenons l'exemple des 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. C'était des processeurs quadruple émission, ce qui fait que la moitié de la largeur du processeur sert pour des opérations entières, et l'autre moitié pour les accès mémoire.
{|class="wikitable"
|-
! Port 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
|}
Les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon, sont un exemple moins représentatif. Ces microarchitectures utilisent le partage des ports d'émission au maximum. C'était des microarchitectures à triple émission, avec six ports d’émission. Il y avait trois ports d'émission pour les opérations flottantes, et trois ports d'émission pour les micro-opérations entières/mémoire. Il y avait trois ALU entières et trois unités mémoire, chaque ALU partageait un port d'émission avec une unité mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements.
{|class="wikitable"
|-
! Port 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 || ALU entière || FADD || FMUL || FPU (autres opérations)
|-
| Multiplieur || || || || ||
|-
| LOAD/STORE || LOAD/STORE || LOAD/STORE || || ||
|}
Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair.
Par exemple, la microarchitecture ''Golden Cove'' a deux ''barrel shifters''.
{|class="wikitable"
|-
! Port 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 !! Port d'émission n°7 !! Port d'émission n°8 !! Port d'émission n°9 !! Port d'émission n°10
|-
| ALU entière || ALU entière || ALU entière || ALU entière || ALU entière || LOAD || LOAD || LOAD || STORE || STORE
|-
| ''Barrel shifter'' || Multiplieur || Multiplieur || ''Barrel shifter'' || || || || || ||
|-
| Branchements || Diviseur || || Branchements || || || || || ||
|}
==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===
Dans le cas le plus simple, les accès mémoire sont traités comme des opérations entières un peu spéciales. L'unité mémoire est sur le même port d'émission que l'ALU entière, ce qui fait que les accès mémoire remplacent des opérations entières. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les CPU superscalaire à exécution dans le désordre==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Les unités d'exécution dans le désordre===
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports de ''dispatch''. Mais avec des stations de réservation, il faut le double des ports de ''schedule''. La raison est que les registres sont lus après émission avec une fenê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 de ''schedule'' que de ''dispatch''.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de 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===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Avec plusieurs dizaines d'unités de calcul différentes, ajouter autant 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.
La première solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Le processeur détecte quand il n'y a pas assez de ports pour servir toutes les micro-opérations. Quand ça arrive, l'unité d'émission émet assez de micro-opérations pour exploiter les ports disponibles, mais met en attente les micro-opérations en trop, le temps que les ports se libèrent. Pour cela, le banc de registre est géré par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Une autre solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une dernière solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
976jm622o6hjz2ittgxwffhqlva0whh
773214
773213
2026-09-25T21:36:19Z
Mewtow
31375
/* Les CPU superscalaires mélangent les trois techniques */
773214
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===L'impact des contraintes d’appariement sur la performance===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour la suite du chapitre===
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==Le ''front-end'' des processeurs superscalaires==
Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple.
Intuitivement, charger, décoder et 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 certaines processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. 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
|}
Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs.
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===L'émission parallèle des instructions===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elle sont propagée au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage).
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
==Les processeurs implémentés avec plusieurs pipelines==
Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Vous vous dites que l'implémentation d'un processeur superscalaire n'est pas la même suivant que le CPU utilise l'exécution dans le désordre, ou non. Mais dans les faits, la distinction est plus subtile que ca. Pour l'expliquer, je vais utiliser une classification originale, qui sépare les processeurs superscalaires en trois sous-types. La distinction se fait sur la manière dont la superscalarité est implémentée en matériel.La classification que j'utilise distingue :
* Les processeurs avec plusieurs pipelines, sans exécution dans le désordre.
* Les processeurs superscalaires avec un pipeline dynamique.
* Les processeurs avec des fenêtres d'instruction décentralisées.
La distinction n'est pas qu'une question d'implémentation matérielle. Elle impacte aussi le ratio entre instructions chargées/décodées et instructions émises. Vous vous dites que les deux sont égales, mais ce n'est le cas que sur les processeurs avec N pipelines ! Sur les autres processeurs, il est possible de décoder 4 instructions, mais d'en émettre maximum 6 par cycle ! Et sur les CPU avec des fenêtres d'instruction décentralisées, il y a aussi une différence entre le nombre d'instructions qui sont ''dispatch'' et ''schedule''.
La catégorie la plus simple est celle des '''processeurs avec plusieurs pipelines'''. Ce sont des processeurs superscalaires sans exécution dans le désordre. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction.
Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, d'une FPU, éventuellement d'un second banc de registres, guère plus. Il permet d'exécuter une seconde opération de calcul, pas plus, ce qui est à l'origine de contraintes d'appariemment assez fortes. De plus, il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section.
===La double émission entière-flottante===
L'exemple le plus simple utilise un pipeline pour les opérations flottantes, séparé du pipeline originel. Pour bien expliquer les choses, prenons le processeur illustré ci-dessous. Un processeur a deux ''back-end'' séparés, un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par l'ALU entière, passent éventuellement dans l'unité mémoire, pour ensuite arriver à l'étape de ''writeback''. La FPU et le banc de registre flottant ont eux aussi leur propre ''back-end'' séparé.
[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]]
Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaqaue pipeline, à chaque cycle.
L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine.
Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants.
===La double émission entière===
Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait.
Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
==Les processeurs superscalaires dynamiques==
Les processeurs précédents, avec 2 pipelines, avaient une unité d'émission assez simple. Elle avait deux ports de décodage, et deux ports d'émission. Le nombre de ports de décodage et d'émission étaient identiques. Vous vous dites que c'est naturel, et intuitif, que c'est normal pour un processeur superscalaire. Mais en réalité, la majorité des processeurs superscalaires a plus de ports d'émission que de décodage. Expliquer pourquoi va cependant nous demander de parler de comment la superscalarité exploite les unités de calcul.
Posons-nous cette question : de combien d'unités un processeur superscalaire a besoin ? Les unités en question ne sont nécessairement des unité de calcul, l'unité mémoire et l'unité de branchement comptent aussi. La réponse dépend du nombre de voies. Si le ''front-end'' charge et décode un paquet de N instructions, exécuter ces N instructions simultanément demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section.
La réponse dépend aussi des contraintes d’appariement. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission parfaite devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement, ce qui ferait passer de 6 unités à 12. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, le cout en circuit encore plus prohibitif !
En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée, alors que la double émission parait plus adaptée.
Et c'est là que la différence entre ports d'émission et de décodage devient intéressante. L'idée est d'avoir une unité d'émission avec deux ports de décodage, et 4 port d'émission (un par unité). Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux micro-opérations tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu.
Les tout premiers CPU superscalaires étaient nombreux à utiliser cette technique. Les exemples classiques sont l'HyperSPARC, de l'alpha 21064 et du PowerPC 603. C'étaient des CPU double émission, avec une ALU entière, une unité mémoire et une FPU. Nous parlerons de '''partitionnement INT-FLOAT-MEM'''. Les contraintes d’appariement variaient grandement suivant le processeur.
Le PowerPC 603 implémentait une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur.
Avant de poursuivre, précisons que la FPU regroupe un additionneur flottant et un multiplieur flottant. Et ce fait peut être exploité pour approfondir le partitionnement. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions).
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple, facile à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. La raison est que le partitionnement permet d'émettre des paires, triplets, quadruplets de micro-opérations qui sont rares en pratique.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''.
Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, 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. Et enfin, il faut rajouter un port d'émission pour pouvoir émettre une micro-opération entière en plus.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle.
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
Reste à connecter les unités de calcul à l'unité d'émission (indirectement, car il y a des étages de pipeline entre les deux). Une implémentation basique utilisee un port d'émission par unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Et chaque unité a son propre port d'émission. La conséquence est que les contraintes d'appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La présence d'un circuit multiplieur et d'un ''barrel shifter'' pose des problèmes pour le partitionnement. Il est possible d'avoir un port d'émission dédié au multiplieur, et un autre au ''barrel shifter'', qui sont séparés de ceux pour l'ALU entière. Il est alors possible d'émettre une opération entière, une multiplication et un décalage, simultanément. Nous parlerons de '''partitionnement entier''' pour désigner cette possibilité.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
Le partitionnement entier est cependant très rare, car le gain en performance serait trop faible au regard des couts. Quelques processeurs historiques l'utilisaient, mais ce n'est plus le cas de nos jours. La raison est que les multiplications et les décalages sont assez rares dans le code, ce qui fait que le port d'émission associés sont sous-utilisés. Et un port d'émission a un cout en circuit assez important. 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.
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul. L'exemple classique est justement lié aux circuits multiplieur et ''barrel shifters''. L'idée est que le multiplieur et le ''barrel shifter'' sont mis sur le même port d'émission qu'une ALU entière.
Prenons par exemple le processeur à double émission entière illustré ci-contre. Un port d'émission est relié à une ALU entière seule, et l'autre port d'émission alimente la seconde ALU entière et le multiplieur. La conséquence est que le processeur peut émettre deux opérations entières, dont une multiplication. Si on avait utilisé un port d'émission dédié au multiplieur, il aurait été possible d'émettre deux opérations entières et une multiplication. On perd donc une voie, mais on gagne en flexibilité et on économise des circuits.
[[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]]
En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais le partage des ports d'émission peut appliquer à n'importe quelle unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. 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.
La solution est de partager des ports d'émission, pour passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple relie une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites. Le CPU peut émettre 6 opérations entières simultanément, grâce à 6 ALU entières. Les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires mélangent les trois techniques===
Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autre unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents.
* Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU.
* La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants.
* La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables.
* Le '''partage des ports d'émission''' réduit le cout en circuit du CPU, pour des pertes de performances mineures si bien fait.
L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. Le partage des ports d'émission a lui aussi mis un peu de temps pour s'imposer. Globalement, les processeurs superscalaires ont évolués progressivement, pour se rapprocher d'une sorte de standard. Mais pour comprendre ce qu'est ce standard, et en quoi les CPU historiques s'en sont progressivement rapprochés, il faut étudier l'histoire des CPU superscalaires.
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais abandonnés avant la production, comme l'ACS-1 d'IBM. Les années 1980 ont vu l'arrivée de prototypes de processeurs VLIW, mais qui n'étaient pas superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail.
Les premiers CPU superscalaires commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. Les CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 hésitaient entre double et quadruple émission. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 ||
|-
! 1995
| 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000
|-
! 1996
| class="f_bleu | 620 || || || || class="f_bleu | 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
Les premiers CPU à double émission sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Voici le tableau chronologique disant quel CPU utilisait quelle technique.
* Les CPU en vert utilisaient la double émission entière-flottante.
* Les CPU en rouge utilisaient la double émission entière "pure".
* Les CPU en jaune utilisaient un ''partitionnement complet'' entre ALU, FPU et unité mémoire.
* Les CPU sans couleur utilisaient un grand nombre d'unités, avec beaucoup de ports d'émission.
* Les CPU barrés utilisent la triple ou quadruple émission et seront vu dans la section suivante.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| class="f_vert | RS/6000 || || || class="f_rouge | i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| class="f_vert | RSC || class="f_vert | PA 7100 || <s>SuperSPARC</s> || || class="f_jaune | 21064 ||
|-
! 1993
| class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || ||
|-
! 1994
| <s>POWER 2</s> || PA 7200 || || || <s>21164</s> ||
|}
C'est en 1994, que la quadruple émission est arrivée et que les CPU ont essayé divers implémentations. Il y avait alors trois stratégies pour la double/triple/quadruple émission, qui partageaient des problèmes similaires.
La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Le POWER 2 dupliquait ses FPUs à la perfection, alors que le 21264 séparait l'additionneur et le multiplieur flottant. Les accès mémoire étaient traités comme des opérations entières et le processeur pouvait souvent en émettre deux à la fois. Mais une telle organisation n'est plus utilisée de nos jours, car, sans instructions flottantes à exécuter, elle est équivalente à de la double émission.
La seconde implémentation utilisait deux ALU entières, une unité mémoire et une FPU. De quoi exécuter en même temps deux opérations entières, une opération flottante, et un accès mémoire. La seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur). Cette organisation est intéressante, mais encore trop stricte pour donner de bons résultats. Les quadruplets avec exactement deux opérations entières, un accès mémoire et une opération flottante sont rares. Par contre, l'usage de ces unités en double ou triple émission a été utilisé sur le PA 7200 et le SuperSPARC. Malheureusement, en plus de problèmes au niveau de son cache, le SuperSPARC avait de nombreuses contraintes d’appariement. Par exemple, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Les contraintes étaient liées au banc de registre, qui n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions.
Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, bien plus que quatre, avec au minimum deux ALU entières. Prenons l'exemple de l'UltraSPARC, qui avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplieur flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes, pour de la quadruple émission.
Dans le même genre, les processeurs MIPS R8000 et R10000 avaient une implémentation plus moderne, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités. Moitié opérations entières, moitié opérations mémoire, avec de quoi remplacer les opérations entières par des opérations flottantes. Le PA 8000, lui, dupliquait toutes ses unités en deux exemplaires, ce qui n'est pas l'idéal, aucun processeur moderne ne fait cela aujourd'hui.
Le tout est résumé dans le tableau ci-dessous, avec :
* en vert les CPU utilisant la première stratégie ;
* en rouge les CPU utilisant la seconde stratégie ;
* en jaune les CPU utilisant la troisième stratégie.
{|class="wikitable"
|+ Processeurs quadruple émission historiques
|-
! Année !! Processeur !! Unités
|-
! 1991
| class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques
|-
|-
! rowspan="2" | 1994
| class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'')
|-
| class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM
|-
! rowspan="3" | 1995
| PA 8000 || 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD
|-
| class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques
|-
| MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM
|-
! rowspan="2" | 1996
| class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM
|-
| class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL)
|}
Malheureusement, si l'idée de base était pas mal, ces CPUs n'avaient pas assez d'unités de calcul entières pour remplir la quadruple émission. Le Motorola 88110, vu en exemple plus haut, était assez similaire, avec pas moins de 10 unités, mais ne faisait que de la double émission. De la double émission pour deux ALU entières, c'est bien plus performant, car on peut remplir une grande partie de la largeur du CPU avec des opérations entières.
Les CPU superscalaires modernes tendent à mettre le paquet niveau opérations arithmétiques, mais pas forcément au point de remplir la totalité de la largeur du processeur. Il est intéressant d'avoir trois ALU entières pour de la triple ou quadruple émission. Mais au-delà, la règle est que pour N voies, le processeur dispose d'au moins N/2 ALU entières. Et N/2 est une borne basse, la plupart des CPU x86 ont environ tournent plutôt autour des 2/3, voire des 3/4. Attention, cela ne signifie pas que le processeur émet systématiquement N/2 opération entières, il peut par exemple remplacer des opérations entières par des opérations flottantes.
En général, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. La conséquence est que les processeurs ont commencé à dupliquer leurs unités mémoire dès qu'ils ont atteint la quadruple émission, pour les CPU grand public. Par exemple, la microarchitecture ''Golden Cove'' d'Intel dispose de cinq unités de calcul, trois unités mémoire (deux spécialisées dans les écritures, trois dans les lectures). La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire. Notez que le ratio est équilibré entre opérations entières et mémoire, avec soit égalité, soit un avantage pour les ALU entières.
Une autre régularité est que la largeur totale du processeur peut être occupée par des opérations entières/mémoire. Pour le dire autrement, sur un processeur quadruple émission, il pourra émettre un mix de 4 instructions entière/mémoire, avec zéro opération flottante. Idem avec des processeurs triples ou pentuple émission, voire plus. Si le processeur émet une opération flottante, elle remplace une opération entière.
Prenons l'exemple des 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. C'était des processeurs quadruple émission, ce qui fait que la moitié de la largeur du processeur sert pour des opérations entières, et l'autre moitié pour les accès mémoire.
{|class="wikitable"
|-
! Port 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
|}
Les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon, sont un exemple moins représentatif. Ces microarchitectures utilisent le partage des ports d'émission au maximum. C'était des microarchitectures à triple émission, avec six ports d’émission. Il y avait trois ports d'émission pour les opérations flottantes, et trois ports d'émission pour les micro-opérations entières/mémoire. Il y avait trois ALU entières et trois unités mémoire, chaque ALU partageait un port d'émission avec une unité mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements.
{|class="wikitable"
|-
! Port 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 || ALU entière || FADD || FMUL || FPU (autres opérations)
|-
| Multiplieur || || || || ||
|-
| LOAD/STORE || LOAD/STORE || LOAD/STORE || || ||
|}
Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair.
Par exemple, la microarchitecture ''Golden Cove'' a deux ''barrel shifters''.
{|class="wikitable"
|-
! Port 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 !! Port d'émission n°7 !! Port d'émission n°8 !! Port d'émission n°9 !! Port d'émission n°10
|-
| ALU entière || ALU entière || ALU entière || ALU entière || ALU entière || LOAD || LOAD || LOAD || STORE || STORE
|-
| ''Barrel shifter'' || Multiplieur || Multiplieur || ''Barrel shifter'' || || || || || ||
|-
| Branchements || Diviseur || || Branchements || || || || || ||
|}
==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===
Dans le cas le plus simple, les accès mémoire sont traités comme des opérations entières un peu spéciales. L'unité mémoire est sur le même port d'émission que l'ALU entière, ce qui fait que les accès mémoire remplacent des opérations entières. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les CPU superscalaire à exécution dans le désordre==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Les unités d'exécution dans le désordre===
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports de ''dispatch''. Mais avec des stations de réservation, il faut le double des ports de ''schedule''. La raison est que les registres sont lus après émission avec une fenê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 de ''schedule'' que de ''dispatch''.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de 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===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Avec plusieurs dizaines d'unités de calcul différentes, ajouter autant 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.
La première solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Le processeur détecte quand il n'y a pas assez de ports pour servir toutes les micro-opérations. Quand ça arrive, l'unité d'émission émet assez de micro-opérations pour exploiter les ports disponibles, mais met en attente les micro-opérations en trop, le temps que les ports se libèrent. Pour cela, le banc de registre est géré par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Une autre solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une dernière solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
ccljqf679q8lc0ysexcvgapgf64gwsr
773215
773214
2026-09-25T21:38:24Z
Mewtow
31375
/* Les CPU superscalaires mélangent les trois techniques */
773215
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===L'impact des contraintes d’appariement sur la performance===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour la suite du chapitre===
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==Le ''front-end'' des processeurs superscalaires==
Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple.
Intuitivement, charger, décoder et 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 certaines processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. 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
|}
Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs.
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===L'émission parallèle des instructions===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elle sont propagée au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage).
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
==Les processeurs implémentés avec plusieurs pipelines==
Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Vous vous dites que l'implémentation d'un processeur superscalaire n'est pas la même suivant que le CPU utilise l'exécution dans le désordre, ou non. Mais dans les faits, la distinction est plus subtile que ca. Pour l'expliquer, je vais utiliser une classification originale, qui sépare les processeurs superscalaires en trois sous-types. La distinction se fait sur la manière dont la superscalarité est implémentée en matériel.La classification que j'utilise distingue :
* Les processeurs avec plusieurs pipelines, sans exécution dans le désordre.
* Les processeurs superscalaires avec un pipeline dynamique.
* Les processeurs avec des fenêtres d'instruction décentralisées.
La distinction n'est pas qu'une question d'implémentation matérielle. Elle impacte aussi le ratio entre instructions chargées/décodées et instructions émises. Vous vous dites que les deux sont égales, mais ce n'est le cas que sur les processeurs avec N pipelines ! Sur les autres processeurs, il est possible de décoder 4 instructions, mais d'en émettre maximum 6 par cycle ! Et sur les CPU avec des fenêtres d'instruction décentralisées, il y a aussi une différence entre le nombre d'instructions qui sont ''dispatch'' et ''schedule''.
La catégorie la plus simple est celle des '''processeurs avec plusieurs pipelines'''. Ce sont des processeurs superscalaires sans exécution dans le désordre. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction.
Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, d'une FPU, éventuellement d'un second banc de registres, guère plus. Il permet d'exécuter une seconde opération de calcul, pas plus, ce qui est à l'origine de contraintes d'appariemment assez fortes. De plus, il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section.
===La double émission entière-flottante===
L'exemple le plus simple utilise un pipeline pour les opérations flottantes, séparé du pipeline originel. Pour bien expliquer les choses, prenons le processeur illustré ci-dessous. Un processeur a deux ''back-end'' séparés, un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par l'ALU entière, passent éventuellement dans l'unité mémoire, pour ensuite arriver à l'étape de ''writeback''. La FPU et le banc de registre flottant ont eux aussi leur propre ''back-end'' séparé.
[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]]
Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaqaue pipeline, à chaque cycle.
L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine.
Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants.
===La double émission entière===
Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait.
Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
==Les processeurs superscalaires dynamiques==
Les processeurs précédents, avec 2 pipelines, avaient une unité d'émission assez simple. Elle avait deux ports de décodage, et deux ports d'émission. Le nombre de ports de décodage et d'émission étaient identiques. Vous vous dites que c'est naturel, et intuitif, que c'est normal pour un processeur superscalaire. Mais en réalité, la majorité des processeurs superscalaires a plus de ports d'émission que de décodage. Expliquer pourquoi va cependant nous demander de parler de comment la superscalarité exploite les unités de calcul.
Posons-nous cette question : de combien d'unités un processeur superscalaire a besoin ? Les unités en question ne sont nécessairement des unité de calcul, l'unité mémoire et l'unité de branchement comptent aussi. La réponse dépend du nombre de voies. Si le ''front-end'' charge et décode un paquet de N instructions, exécuter ces N instructions simultanément demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section.
La réponse dépend aussi des contraintes d’appariement. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission parfaite devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement, ce qui ferait passer de 6 unités à 12. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, le cout en circuit encore plus prohibitif !
En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée, alors que la double émission parait plus adaptée.
Et c'est là que la différence entre ports d'émission et de décodage devient intéressante. L'idée est d'avoir une unité d'émission avec deux ports de décodage, et 4 port d'émission (un par unité). Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux micro-opérations tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu.
Les tout premiers CPU superscalaires étaient nombreux à utiliser cette technique. Les exemples classiques sont l'HyperSPARC, de l'alpha 21064 et du PowerPC 603. C'étaient des CPU double émission, avec une ALU entière, une unité mémoire et une FPU. Nous parlerons de '''partitionnement INT-FLOAT-MEM'''. Les contraintes d’appariement variaient grandement suivant le processeur.
Le PowerPC 603 implémentait une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur.
Avant de poursuivre, précisons que la FPU regroupe un additionneur flottant et un multiplieur flottant. Et ce fait peut être exploité pour approfondir le partitionnement. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions).
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple, facile à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. La raison est que le partitionnement permet d'émettre des paires, triplets, quadruplets de micro-opérations qui sont rares en pratique.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''.
Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, 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. Et enfin, il faut rajouter un port d'émission pour pouvoir émettre une micro-opération entière en plus.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle.
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
Reste à connecter les unités de calcul à l'unité d'émission (indirectement, car il y a des étages de pipeline entre les deux). Une implémentation basique utilisee un port d'émission par unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Et chaque unité a son propre port d'émission. La conséquence est que les contraintes d'appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La présence d'un circuit multiplieur et d'un ''barrel shifter'' pose des problèmes pour le partitionnement. Il est possible d'avoir un port d'émission dédié au multiplieur, et un autre au ''barrel shifter'', qui sont séparés de ceux pour l'ALU entière. Il est alors possible d'émettre une opération entière, une multiplication et un décalage, simultanément. Nous parlerons de '''partitionnement entier''' pour désigner cette possibilité.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
Le partitionnement entier est cependant très rare, car le gain en performance serait trop faible au regard des couts. Quelques processeurs historiques l'utilisaient, mais ce n'est plus le cas de nos jours. La raison est que les multiplications et les décalages sont assez rares dans le code, ce qui fait que le port d'émission associés sont sous-utilisés. Et un port d'émission a un cout en circuit assez important. 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.
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul. L'exemple classique est justement lié aux circuits multiplieur et ''barrel shifters''. L'idée est que le multiplieur et le ''barrel shifter'' sont mis sur le même port d'émission qu'une ALU entière.
Prenons par exemple le processeur à double émission entière illustré ci-contre. Un port d'émission est relié à une ALU entière seule, et l'autre port d'émission alimente la seconde ALU entière et le multiplieur. La conséquence est que le processeur peut émettre deux opérations entières, dont une multiplication. Si on avait utilisé un port d'émission dédié au multiplieur, il aurait été possible d'émettre deux opérations entières et une multiplication. On perd donc une voie, mais on gagne en flexibilité et on économise des circuits.
[[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]]
En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais le partage des ports d'émission peut appliquer à n'importe quelle unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. 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.
La solution est de partager des ports d'émission, pour passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple relie une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites. Le CPU peut émettre 6 opérations entières simultanément, grâce à 6 ALU entières. Les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires mélangent les trois techniques===
Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autre unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents.
* Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU.
* La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants.
* La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables.
* Le '''partage des ports d'émission''' réduit le cout en circuit du CPU, pour des pertes de performances mineures si bien fait.
L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. Le partage des ports d'émission a lui aussi mis un peu de temps pour s'imposer. Globalement, les processeurs superscalaires ont évolués progressivement, pour se rapprocher d'une sorte de standard. Mais pour comprendre ce qu'est ce standard, et en quoi les CPU historiques s'en sont progressivement rapprochés, il faut étudier l'histoire des CPU superscalaires.
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais abandonnés avant la production, comme l'ACS-1 d'IBM. Les années 1980 ont vu l'arrivée de prototypes de processeurs VLIW, mais qui n'étaient pas superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail.
Les premiers CPU superscalaires commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. Les CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 hésitaient entre double et quadruple émission. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 ||
|-
! 1995
| 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000
|-
! 1996
| class="f_bleu | 620 || || || || class="f_bleu | 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
Les premiers CPU à double émission sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Voici le tableau chronologique disant quel CPU utilisait quelle technique.
* Les CPU en vert utilisaient la double émission entière-flottante.
* Les CPU en rouge utilisaient la double émission entière "pure".
* Les CPU en jaune utilisaient un ''partitionnement complet'' entre ALU, FPU et unité mémoire.
* Les CPU sans couleur utilisaient un grand nombre d'unités, avec beaucoup de ports d'émission.
* Les CPU barrés utilisent la triple ou quadruple émission et seront vu dans la section suivante.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| class="f_vert | RS/6000 || || || class="f_rouge | i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| class="f_vert | RSC || class="f_vert | PA 7100 || <s>SuperSPARC</s> || || class="f_jaune | 21064 ||
|-
! 1993
| class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || ||
|-
! 1994
| <s>POWER 2</s> || PA 7200 || || || <s>21164</s> ||
|}
C'est en 1994, que la quadruple émission est arrivée et que les CPU ont essayé divers implémentations. Il y avait alors trois stratégies pour la double/triple/quadruple émission, qui partageaient des problèmes similaires.
La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Le POWER 2 dupliquait ses FPUs à la perfection, alors que le 21264 séparait l'additionneur et le multiplieur flottant. Les accès mémoire étaient traités comme des opérations entières et le processeur pouvait souvent en émettre deux à la fois. Mais une telle organisation n'est plus utilisée de nos jours, car, sans instructions flottantes à exécuter, elle est équivalente à de la double émission.
La seconde implémentation utilisait deux ALU entières (dont un multiplieur), une unité mémoire et une FPU.Cette organisation est intéressante, mais encore trop stricte pour donner de bons résultats, les quadruplets avec exactement deux opérations entières, un accès mémoire et une opération flottante étant trop rares. L'usage de ces 4 unités en double ou triple émission a été utilisé sur le PA 7200 et le SuperSPARC. Malheureusement, en plus de problèmes au niveau de son cache, le SuperSPARC avait de nombreuses contraintes d’appariement, car le banc de registre n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions. De plus, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop.
Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, bien plus que quatre, avec au minimum deux ALU entières. Prenons l'exemple de l'UltraSPARC, qui avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplieur flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes, pour de la quadruple émission.
Dans le même genre, les processeurs MIPS R8000 et R10000 avaient une implémentation plus moderne, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités. Moitié opérations entières, moitié opérations mémoire, avec de quoi remplacer les opérations entières par des opérations flottantes. Le PA 8000, lui, dupliquait toutes ses unités en deux exemplaires, ce qui n'est pas l'idéal, aucun processeur moderne ne fait cela aujourd'hui.
Le tout est résumé dans le tableau ci-dessous, avec :
* en vert les CPU utilisant la première stratégie ;
* en rouge les CPU utilisant la seconde stratégie ;
* en jaune les CPU utilisant la troisième stratégie.
{|class="wikitable"
|+ Processeurs quadruple émission historiques
|-
! Année !! Processeur !! Unités
|-
! 1991
| class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques
|-
|-
! rowspan="2" | 1994
| class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'')
|-
| class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM
|-
! rowspan="3" | 1995
| PA 8000 || 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD
|-
| class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques
|-
| MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM
|-
! rowspan="2" | 1996
| class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM
|-
| class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL)
|}
Malheureusement, si l'idée de base était pas mal, ces CPUs n'avaient pas assez d'unités de calcul entières pour remplir la quadruple émission. Le Motorola 88110, vu en exemple plus haut, était assez similaire, avec pas moins de 10 unités, mais ne faisait que de la double émission. De la double émission pour deux ALU entières, c'est bien plus performant, car on peut remplir une grande partie de la largeur du CPU avec des opérations entières.
Les CPU superscalaires modernes tendent à mettre le paquet niveau opérations arithmétiques, mais pas forcément au point de remplir la totalité de la largeur du processeur. Il est intéressant d'avoir trois ALU entières pour de la triple ou quadruple émission. Mais au-delà, la règle est que pour N voies, le processeur dispose d'au moins N/2 ALU entières. Et N/2 est une borne basse, la plupart des CPU x86 ont environ tournent plutôt autour des 2/3, voire des 3/4. Attention, cela ne signifie pas que le processeur émet systématiquement N/2 opération entières, il peut par exemple remplacer des opérations entières par des opérations flottantes.
En général, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. La conséquence est que les processeurs ont commencé à dupliquer leurs unités mémoire dès qu'ils ont atteint la quadruple émission, pour les CPU grand public. Par exemple, la microarchitecture ''Golden Cove'' d'Intel dispose de cinq unités de calcul, trois unités mémoire (deux spécialisées dans les écritures, trois dans les lectures). La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire. Notez que le ratio est équilibré entre opérations entières et mémoire, avec soit égalité, soit un avantage pour les ALU entières.
Une autre régularité est que la largeur totale du processeur peut être occupée par des opérations entières/mémoire. Pour le dire autrement, sur un processeur quadruple émission, il pourra émettre un mix de 4 instructions entière/mémoire, avec zéro opération flottante. Idem avec des processeurs triples ou pentuple émission, voire plus. Si le processeur émet une opération flottante, elle remplace une opération entière.
Prenons l'exemple des 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. C'était des processeurs quadruple émission, ce qui fait que la moitié de la largeur du processeur sert pour des opérations entières, et l'autre moitié pour les accès mémoire.
{|class="wikitable"
|-
! Port 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
|}
Les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon, sont un exemple moins représentatif. Ces microarchitectures utilisent le partage des ports d'émission au maximum. C'était des microarchitectures à triple émission, avec six ports d’émission. Il y avait trois ports d'émission pour les opérations flottantes, et trois ports d'émission pour les micro-opérations entières/mémoire. Il y avait trois ALU entières et trois unités mémoire, chaque ALU partageait un port d'émission avec une unité mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements.
{|class="wikitable"
|-
! Port 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 || ALU entière || FADD || FMUL || FPU (autres opérations)
|-
| Multiplieur || || || || ||
|-
| LOAD/STORE || LOAD/STORE || LOAD/STORE || || ||
|}
Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair.
Par exemple, la microarchitecture ''Golden Cove'' a deux ''barrel shifters''.
{|class="wikitable"
|-
! Port 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 !! Port d'émission n°7 !! Port d'émission n°8 !! Port d'émission n°9 !! Port d'émission n°10
|-
| ALU entière || ALU entière || ALU entière || ALU entière || ALU entière || LOAD || LOAD || LOAD || STORE || STORE
|-
| ''Barrel shifter'' || Multiplieur || Multiplieur || ''Barrel shifter'' || || || || || ||
|-
| Branchements || Diviseur || || Branchements || || || || || ||
|}
==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===
Dans le cas le plus simple, les accès mémoire sont traités comme des opérations entières un peu spéciales. L'unité mémoire est sur le même port d'émission que l'ALU entière, ce qui fait que les accès mémoire remplacent des opérations entières. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les CPU superscalaire à exécution dans le désordre==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Les unités d'exécution dans le désordre===
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports de ''dispatch''. Mais avec des stations de réservation, il faut le double des ports de ''schedule''. La raison est que les registres sont lus après émission avec une fenê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 de ''schedule'' que de ''dispatch''.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de 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===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Avec plusieurs dizaines d'unités de calcul différentes, ajouter autant 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.
La première solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Le processeur détecte quand il n'y a pas assez de ports pour servir toutes les micro-opérations. Quand ça arrive, l'unité d'émission émet assez de micro-opérations pour exploiter les ports disponibles, mais met en attente les micro-opérations en trop, le temps que les ports se libèrent. Pour cela, le banc de registre est géré par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Une autre solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une dernière solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
77etk57tvscipgter7js6uu9pcs6jdv
773216
773215
2026-09-25T21:40:02Z
Mewtow
31375
/* Les CPU superscalaires mélangent les trois techniques */
773216
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===L'impact des contraintes d’appariement sur la performance===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour la suite du chapitre===
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==Le ''front-end'' des processeurs superscalaires==
Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple.
Intuitivement, charger, décoder et 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 certaines processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. 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
|}
Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs.
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===L'émission parallèle des instructions===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elle sont propagée au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage).
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
==Les processeurs implémentés avec plusieurs pipelines==
Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Vous vous dites que l'implémentation d'un processeur superscalaire n'est pas la même suivant que le CPU utilise l'exécution dans le désordre, ou non. Mais dans les faits, la distinction est plus subtile que ca. Pour l'expliquer, je vais utiliser une classification originale, qui sépare les processeurs superscalaires en trois sous-types. La distinction se fait sur la manière dont la superscalarité est implémentée en matériel.La classification que j'utilise distingue :
* Les processeurs avec plusieurs pipelines, sans exécution dans le désordre.
* Les processeurs superscalaires avec un pipeline dynamique.
* Les processeurs avec des fenêtres d'instruction décentralisées.
La distinction n'est pas qu'une question d'implémentation matérielle. Elle impacte aussi le ratio entre instructions chargées/décodées et instructions émises. Vous vous dites que les deux sont égales, mais ce n'est le cas que sur les processeurs avec N pipelines ! Sur les autres processeurs, il est possible de décoder 4 instructions, mais d'en émettre maximum 6 par cycle ! Et sur les CPU avec des fenêtres d'instruction décentralisées, il y a aussi une différence entre le nombre d'instructions qui sont ''dispatch'' et ''schedule''.
La catégorie la plus simple est celle des '''processeurs avec plusieurs pipelines'''. Ce sont des processeurs superscalaires sans exécution dans le désordre. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction.
Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, d'une FPU, éventuellement d'un second banc de registres, guère plus. Il permet d'exécuter une seconde opération de calcul, pas plus, ce qui est à l'origine de contraintes d'appariemment assez fortes. De plus, il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section.
===La double émission entière-flottante===
L'exemple le plus simple utilise un pipeline pour les opérations flottantes, séparé du pipeline originel. Pour bien expliquer les choses, prenons le processeur illustré ci-dessous. Un processeur a deux ''back-end'' séparés, un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par l'ALU entière, passent éventuellement dans l'unité mémoire, pour ensuite arriver à l'étape de ''writeback''. La FPU et le banc de registre flottant ont eux aussi leur propre ''back-end'' séparé.
[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]]
Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaqaue pipeline, à chaque cycle.
L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine.
Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants.
===La double émission entière===
Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait.
Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
==Les processeurs superscalaires dynamiques==
Les processeurs précédents, avec 2 pipelines, avaient une unité d'émission assez simple. Elle avait deux ports de décodage, et deux ports d'émission. Le nombre de ports de décodage et d'émission étaient identiques. Vous vous dites que c'est naturel, et intuitif, que c'est normal pour un processeur superscalaire. Mais en réalité, la majorité des processeurs superscalaires a plus de ports d'émission que de décodage. Expliquer pourquoi va cependant nous demander de parler de comment la superscalarité exploite les unités de calcul.
Posons-nous cette question : de combien d'unités un processeur superscalaire a besoin ? Les unités en question ne sont nécessairement des unité de calcul, l'unité mémoire et l'unité de branchement comptent aussi. La réponse dépend du nombre de voies. Si le ''front-end'' charge et décode un paquet de N instructions, exécuter ces N instructions simultanément demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section.
La réponse dépend aussi des contraintes d’appariement. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission parfaite devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement, ce qui ferait passer de 6 unités à 12. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, le cout en circuit encore plus prohibitif !
En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée, alors que la double émission parait plus adaptée.
Et c'est là que la différence entre ports d'émission et de décodage devient intéressante. L'idée est d'avoir une unité d'émission avec deux ports de décodage, et 4 port d'émission (un par unité). Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux micro-opérations tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu.
Les tout premiers CPU superscalaires étaient nombreux à utiliser cette technique. Les exemples classiques sont l'HyperSPARC, de l'alpha 21064 et du PowerPC 603. C'étaient des CPU double émission, avec une ALU entière, une unité mémoire et une FPU. Nous parlerons de '''partitionnement INT-FLOAT-MEM'''. Les contraintes d’appariement variaient grandement suivant le processeur.
Le PowerPC 603 implémentait une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur.
Avant de poursuivre, précisons que la FPU regroupe un additionneur flottant et un multiplieur flottant. Et ce fait peut être exploité pour approfondir le partitionnement. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions).
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple, facile à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. La raison est que le partitionnement permet d'émettre des paires, triplets, quadruplets de micro-opérations qui sont rares en pratique.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''.
Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, 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. Et enfin, il faut rajouter un port d'émission pour pouvoir émettre une micro-opération entière en plus.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle.
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
Reste à connecter les unités de calcul à l'unité d'émission (indirectement, car il y a des étages de pipeline entre les deux). Une implémentation basique utilisee un port d'émission par unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Et chaque unité a son propre port d'émission. La conséquence est que les contraintes d'appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La présence d'un circuit multiplieur et d'un ''barrel shifter'' pose des problèmes pour le partitionnement. Il est possible d'avoir un port d'émission dédié au multiplieur, et un autre au ''barrel shifter'', qui sont séparés de ceux pour l'ALU entière. Il est alors possible d'émettre une opération entière, une multiplication et un décalage, simultanément. Nous parlerons de '''partitionnement entier''' pour désigner cette possibilité.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
Le partitionnement entier est cependant très rare, car le gain en performance serait trop faible au regard des couts. Quelques processeurs historiques l'utilisaient, mais ce n'est plus le cas de nos jours. La raison est que les multiplications et les décalages sont assez rares dans le code, ce qui fait que le port d'émission associés sont sous-utilisés. Et un port d'émission a un cout en circuit assez important. 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.
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul. L'exemple classique est justement lié aux circuits multiplieur et ''barrel shifters''. L'idée est que le multiplieur et le ''barrel shifter'' sont mis sur le même port d'émission qu'une ALU entière.
Prenons par exemple le processeur à double émission entière illustré ci-contre. Un port d'émission est relié à une ALU entière seule, et l'autre port d'émission alimente la seconde ALU entière et le multiplieur. La conséquence est que le processeur peut émettre deux opérations entières, dont une multiplication. Si on avait utilisé un port d'émission dédié au multiplieur, il aurait été possible d'émettre deux opérations entières et une multiplication. On perd donc une voie, mais on gagne en flexibilité et on économise des circuits.
[[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]]
En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais le partage des ports d'émission peut appliquer à n'importe quelle unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. 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.
La solution est de partager des ports d'émission, pour passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple relie une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites. Le CPU peut émettre 6 opérations entières simultanément, grâce à 6 ALU entières. Les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires mélangent les trois techniques===
Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autre unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents.
* Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU.
* La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants.
* La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables.
* Le '''partage des ports d'émission''' réduit le cout en circuit du CPU, pour des pertes de performances mineures si bien fait.
L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. Le partage des ports d'émission a lui aussi mis un peu de temps pour s'imposer. Globalement, les processeurs superscalaires ont évolués progressivement, pour se rapprocher d'une sorte de standard. Mais pour comprendre ce qu'est ce standard, et en quoi les CPU historiques s'en sont progressivement rapprochés, il faut étudier l'histoire des CPU superscalaires.
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais abandonnés avant la production, comme l'ACS-1 d'IBM. Les années 1980 ont vu l'arrivée de prototypes de processeurs VLIW, mais qui n'étaient pas superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail.
Les premiers CPU superscalaires commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. Les CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 hésitaient entre double et quadruple émission. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 ||
|-
! 1995
| 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000
|-
! 1996
| class="f_bleu | 620 || || || || class="f_bleu | 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
Les premiers CPU à double émission sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Voici le tableau chronologique disant quel CPU utilisait quelle technique.
* Les CPU en vert utilisaient la double émission entière-flottante.
* Les CPU en rouge utilisaient la double émission entière "pure".
* Les CPU en jaune utilisaient un ''partitionnement complet'' entre ALU, FPU et unité mémoire.
* Les CPU sans couleur utilisaient un grand nombre d'unités, avec beaucoup de ports d'émission.
* Les CPU barrés utilisent la triple ou quadruple émission et seront vu dans la section suivante.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| class="f_vert | RS/6000 || || || class="f_rouge | i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| class="f_vert | RSC || class="f_vert | PA 7100 || <s>SuperSPARC</s> || || class="f_jaune | 21064 ||
|-
! 1993
| class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || ||
|-
! 1994
| <s>POWER 2</s> || PA 7200 || || || <s>21164</s> ||
|}
C'est en 1994, que la quadruple émission est arrivée et que les CPU ont essayé divers implémentations. Il y avait alors trois stratégies pour la double/triple/quadruple émission, qui partageaient des problèmes similaires.
La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Le POWER 2 dupliquait ses FPUs à la perfection, alors que le 21264 séparait l'additionneur et le multiplieur flottant. Les accès mémoire étaient traités comme des opérations entières et le processeur pouvait souvent en émettre deux à la fois. Mais une telle organisation n'est plus utilisée de nos jours, car, sans instructions flottantes à exécuter, elle est équivalente à de la double émission.
La seconde implémentation utilisait deux ALU entières (dont un multiplieur), une unité mémoire et une FPU.Cette organisation est intéressante, mais encore trop stricte pour donner de bons résultats, les quadruplets avec exactement deux opérations entières, un accès mémoire et une opération flottante étant trop rares. L'usage de ces 4 unités en double ou triple émission a été utilisé sur le PA 7200 et le SuperSPARC. Malheureusement, en plus de problèmes au niveau de son cache, le SuperSPARC avait de nombreuses contraintes d’appariement, car le banc de registre n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions. De plus, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop.
Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, bien plus que quatre, avec au minimum deux ALU entières. Prenons l'exemple de l'UltraSPARC, qui avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplieur flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes, pour de la quadruple émission.
Dans le même genre, les processeurs MIPS R8000 et R10000 avaient une implémentation plus moderne, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités. Moitié opérations entières, moitié opérations mémoire, avec de quoi remplacer les opérations entières par des opérations flottantes. Le PA 8000, lui, dupliquait toutes ses unités en deux exemplaires, ce qui n'est pas l'idéal, aucun processeur moderne ne fait cela aujourd'hui.
Le tout est résumé dans le tableau ci-dessous, avec :
* en vert les CPU utilisant la première stratégie ;
* en rouge les CPU utilisant la seconde stratégie ;
* en jaune les CPU utilisant la troisième stratégie.
{|class="wikitable"
|+ Processeurs quadruple émission historiques
|-
! Année !! Processeur !! Unités
|-
! 1991
| class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques
|-
|-
! rowspan="2" | 1994
| class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'')
|-
| class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM
|-
! rowspan="3" | 1995
| PA 8000 || 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD
|-
| class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques
|-
| MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM
|-
! rowspan="2" | 1996
| class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM
|-
| class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL)
|}
Malheureusement, si l'idée de base était pas mal, ces CPUs n'avaient pas assez d'unités de calcul entières pour remplir la quadruple émission. Les CPU superscalaires modernes tendent à mettre le paquet niveau opérations arithmétiques, mais pas forcément au point de remplir la totalité de la largeur du processeur. Il est intéressant d'avoir trois ALU entières pour de la triple ou quadruple émission. Mais au-delà, la règle est que pour N voies, le processeur dispose d'au moins N/2 ALU entières. Et N/2 est une borne basse, la plupart des CPU x86 ont environ tournent plutôt autour des 2/3, voire des 3/4. Attention, cela ne signifie pas que le processeur émet systématiquement N/2 opération entières, il peut par exemple remplacer des opérations entières par des opérations flottantes.
En général, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. La conséquence est que les processeurs ont commencé à dupliquer leurs unités mémoire dès qu'ils ont atteint la quadruple émission, pour les CPU grand public. Par exemple, la microarchitecture ''Golden Cove'' d'Intel dispose de cinq unités de calcul, trois unités mémoire (deux spécialisées dans les écritures, trois dans les lectures). La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire. Notez que le ratio est équilibré entre opérations entières et mémoire, avec soit égalité, soit un avantage pour les ALU entières.
Une autre régularité est que la largeur totale du processeur peut être occupée par des opérations entières/mémoire. Pour le dire autrement, sur un processeur quadruple émission, il pourra émettre un mix de 4 instructions entière/mémoire, avec zéro opération flottante. Idem avec des processeurs triples ou pentuple émission, voire plus. Si le processeur émet une opération flottante, elle remplace une opération entière.
Prenons l'exemple des 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. C'était des processeurs quadruple émission, ce qui fait que la moitié de la largeur du processeur sert pour des opérations entières, et l'autre moitié pour les accès mémoire.
{|class="wikitable"
|-
! Port 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
|}
Les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon, sont un exemple moins représentatif. Ces microarchitectures utilisent le partage des ports d'émission au maximum. C'était des microarchitectures à triple émission, avec six ports d’émission. Il y avait trois ports d'émission pour les opérations flottantes, et trois ports d'émission pour les micro-opérations entières/mémoire. Il y avait trois ALU entières et trois unités mémoire, chaque ALU partageait un port d'émission avec une unité mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements.
{|class="wikitable"
|-
! Port 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 || ALU entière || FADD || FMUL || FPU (autres opérations)
|-
| Multiplieur || || || || ||
|-
| LOAD/STORE || LOAD/STORE || LOAD/STORE || || ||
|}
Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair.
Par exemple, la microarchitecture ''Golden Cove'' a deux ''barrel shifters''.
{|class="wikitable"
|-
! Port 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 !! Port d'émission n°7 !! Port d'émission n°8 !! Port d'émission n°9 !! Port d'émission n°10
|-
| ALU entière || ALU entière || ALU entière || ALU entière || ALU entière || LOAD || LOAD || LOAD || STORE || STORE
|-
| ''Barrel shifter'' || Multiplieur || Multiplieur || ''Barrel shifter'' || || || || || ||
|-
| Branchements || Diviseur || || Branchements || || || || || ||
|}
==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===
Dans le cas le plus simple, les accès mémoire sont traités comme des opérations entières un peu spéciales. L'unité mémoire est sur le même port d'émission que l'ALU entière, ce qui fait que les accès mémoire remplacent des opérations entières. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les CPU superscalaire à exécution dans le désordre==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Les unités d'exécution dans le désordre===
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports de ''dispatch''. Mais avec des stations de réservation, il faut le double des ports de ''schedule''. La raison est que les registres sont lus après émission avec une fenê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 de ''schedule'' que de ''dispatch''.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de 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===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Avec plusieurs dizaines d'unités de calcul différentes, ajouter autant 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.
La première solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Le processeur détecte quand il n'y a pas assez de ports pour servir toutes les micro-opérations. Quand ça arrive, l'unité d'émission émet assez de micro-opérations pour exploiter les ports disponibles, mais met en attente les micro-opérations en trop, le temps que les ports se libèrent. Pour cela, le banc de registre est géré par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Une autre solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une dernière solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
ldux0bizxtwuz3jqazvw2jgg8mafge7
773217
773216
2026-09-25T21:48:55Z
Mewtow
31375
/* Les CPU superscalaires mélangent les trois techniques */
773217
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
===L'impact des contraintes d’appariement sur la performance===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===Terminologie pour la suite du chapitre===
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
==Le ''front-end'' des processeurs superscalaires==
Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple.
Intuitivement, charger, décoder et 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 certaines processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. 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
|}
Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs.
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
===L'émission parallèle des instructions===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elle sont propagée au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage).
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
==Les processeurs implémentés avec plusieurs pipelines==
Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Vous vous dites que l'implémentation d'un processeur superscalaire n'est pas la même suivant que le CPU utilise l'exécution dans le désordre, ou non. Mais dans les faits, la distinction est plus subtile que ca. Pour l'expliquer, je vais utiliser une classification originale, qui sépare les processeurs superscalaires en trois sous-types. La distinction se fait sur la manière dont la superscalarité est implémentée en matériel.La classification que j'utilise distingue :
* Les processeurs avec plusieurs pipelines, sans exécution dans le désordre.
* Les processeurs superscalaires avec un pipeline dynamique.
* Les processeurs avec des fenêtres d'instruction décentralisées.
La distinction n'est pas qu'une question d'implémentation matérielle. Elle impacte aussi le ratio entre instructions chargées/décodées et instructions émises. Vous vous dites que les deux sont égales, mais ce n'est le cas que sur les processeurs avec N pipelines ! Sur les autres processeurs, il est possible de décoder 4 instructions, mais d'en émettre maximum 6 par cycle ! Et sur les CPU avec des fenêtres d'instruction décentralisées, il y a aussi une différence entre le nombre d'instructions qui sont ''dispatch'' et ''schedule''.
La catégorie la plus simple est celle des '''processeurs avec plusieurs pipelines'''. Ce sont des processeurs superscalaires sans exécution dans le désordre. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction.
Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, d'une FPU, éventuellement d'un second banc de registres, guère plus. Il permet d'exécuter une seconde opération de calcul, pas plus, ce qui est à l'origine de contraintes d'appariemment assez fortes. De plus, il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section.
===La double émission entière-flottante===
L'exemple le plus simple utilise un pipeline pour les opérations flottantes, séparé du pipeline originel. Pour bien expliquer les choses, prenons le processeur illustré ci-dessous. Un processeur a deux ''back-end'' séparés, un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par l'ALU entière, passent éventuellement dans l'unité mémoire, pour ensuite arriver à l'étape de ''writeback''. La FPU et le banc de registre flottant ont eux aussi leur propre ''back-end'' séparé.
[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]]
Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaqaue pipeline, à chaque cycle.
L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine.
Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants.
===La double émission entière===
Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait.
Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
==Les processeurs superscalaires dynamiques==
Les processeurs précédents, avec 2 pipelines, avaient une unité d'émission assez simple. Elle avait deux ports de décodage, et deux ports d'émission. Le nombre de ports de décodage et d'émission étaient identiques. Vous vous dites que c'est naturel, et intuitif, que c'est normal pour un processeur superscalaire. Mais en réalité, la majorité des processeurs superscalaires a plus de ports d'émission que de décodage. Expliquer pourquoi va cependant nous demander de parler de comment la superscalarité exploite les unités de calcul.
Posons-nous cette question : de combien d'unités un processeur superscalaire a besoin ? Les unités en question ne sont nécessairement des unité de calcul, l'unité mémoire et l'unité de branchement comptent aussi. La réponse dépend du nombre de voies. Si le ''front-end'' charge et décode un paquet de N instructions, exécuter ces N instructions simultanément demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section.
La réponse dépend aussi des contraintes d’appariement. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission parfaite devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement, ce qui ferait passer de 6 unités à 12. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, le cout en circuit encore plus prohibitif !
En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée, alors que la double émission parait plus adaptée.
Et c'est là que la différence entre ports d'émission et de décodage devient intéressante. L'idée est d'avoir une unité d'émission avec deux ports de décodage, et 4 port d'émission (un par unité). Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux micro-opérations tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu.
Les tout premiers CPU superscalaires étaient nombreux à utiliser cette technique. Les exemples classiques sont l'HyperSPARC, de l'alpha 21064 et du PowerPC 603. C'étaient des CPU double émission, avec une ALU entière, une unité mémoire et une FPU. Nous parlerons de '''partitionnement INT-FLOAT-MEM'''. Les contraintes d’appariement variaient grandement suivant le processeur.
Le PowerPC 603 implémentait une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur.
Avant de poursuivre, précisons que la FPU regroupe un additionneur flottant et un multiplieur flottant. Et ce fait peut être exploité pour approfondir le partitionnement. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions).
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple, facile à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. La raison est que le partitionnement permet d'émettre des paires, triplets, quadruplets de micro-opérations qui sont rares en pratique.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''.
Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, 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. Et enfin, il faut rajouter un port d'émission pour pouvoir émettre une micro-opération entière en plus.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle.
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
Reste à connecter les unités de calcul à l'unité d'émission (indirectement, car il y a des étages de pipeline entre les deux). Une implémentation basique utilisee un port d'émission par unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Et chaque unité a son propre port d'émission. La conséquence est que les contraintes d'appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La présence d'un circuit multiplieur et d'un ''barrel shifter'' pose des problèmes pour le partitionnement. Il est possible d'avoir un port d'émission dédié au multiplieur, et un autre au ''barrel shifter'', qui sont séparés de ceux pour l'ALU entière. Il est alors possible d'émettre une opération entière, une multiplication et un décalage, simultanément. Nous parlerons de '''partitionnement entier''' pour désigner cette possibilité.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
Le partitionnement entier est cependant très rare, car le gain en performance serait trop faible au regard des couts. Quelques processeurs historiques l'utilisaient, mais ce n'est plus le cas de nos jours. La raison est que les multiplications et les décalages sont assez rares dans le code, ce qui fait que le port d'émission associés sont sous-utilisés. Et un port d'émission a un cout en circuit assez important. 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.
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul. L'exemple classique est justement lié aux circuits multiplieur et ''barrel shifters''. L'idée est que le multiplieur et le ''barrel shifter'' sont mis sur le même port d'émission qu'une ALU entière.
Prenons par exemple le processeur à double émission entière illustré ci-contre. Un port d'émission est relié à une ALU entière seule, et l'autre port d'émission alimente la seconde ALU entière et le multiplieur. La conséquence est que le processeur peut émettre deux opérations entières, dont une multiplication. Si on avait utilisé un port d'émission dédié au multiplieur, il aurait été possible d'émettre deux opérations entières et une multiplication. On perd donc une voie, mais on gagne en flexibilité et on économise des circuits.
[[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]]
En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais le partage des ports d'émission peut appliquer à n'importe quelle unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. 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.
La solution est de partager des ports d'émission, pour passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple relie une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites. Le CPU peut émettre 6 opérations entières simultanément, grâce à 6 ALU entières. Les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires mélangent les trois techniques===
Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autre unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents.
* Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU.
* La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants.
* La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables.
* Le '''partage des ports d'émission''' réduit le cout en circuit du CPU, pour des pertes de performances mineures si bien fait.
L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. Le partage des ports d'émission a lui aussi mis un peu de temps pour s'imposer. Globalement, les processeurs superscalaires ont évolués progressivement, pour se rapprocher d'une sorte de standard. Mais pour comprendre ce qu'est ce standard, et en quoi les CPU historiques s'en sont progressivement rapprochés, il faut étudier l'histoire des CPU superscalaires.
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais abandonnés avant la production, comme l'ACS-1 d'IBM. Les années 1980 ont vu l'arrivée de prototypes de processeurs VLIW, mais qui n'étaient pas superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail.
Les premiers CPU superscalaires commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. Les CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 hésitaient entre double et quadruple émission. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 ||
|-
! 1995
| 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000
|-
! 1996
| class="f_bleu | 620 || || || || class="f_bleu | 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
Les premiers CPU à double émission sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Voici le tableau chronologique disant quel CPU utilisait quelle technique.
* Les CPU en vert utilisaient la double émission entière-flottante.
* Les CPU en rouge utilisaient la double émission entière "pure".
* Les CPU en jaune utilisaient un ''partitionnement complet'' entre ALU, FPU et unité mémoire.
* Les CPU sans couleur utilisaient un grand nombre d'unités, avec beaucoup de ports d'émission.
* Les CPU barrés utilisent la triple ou quadruple émission et seront vu dans la section suivante.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| class="f_vert | RS/6000 || || || class="f_rouge | i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| class="f_vert | RSC || class="f_vert | PA 7100 || <s>SuperSPARC</s> || || class="f_jaune | 21064 ||
|-
! 1993
| class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || ||
|-
! 1994
| <s>POWER 2</s> || PA 7200 || || || <s>21164</s> ||
|}
C'est en 1994, que la quadruple émission est arrivée et que les CPU ont essayé divers implémentations. Il y avait alors trois stratégies pour la double/triple/quadruple émission, qui partageaient des problèmes similaires.
La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Le POWER 2 dupliquait ses FPUs à la perfection, alors que le 21264 séparait l'additionneur et le multiplieur flottant. Les accès mémoire étaient traités comme des opérations entières et le processeur pouvait souvent en émettre deux à la fois. Mais une telle organisation n'est plus utilisée de nos jours, car, sans instructions flottantes à exécuter, elle est équivalente à de la double émission.
La seconde implémentation utilisait deux ALU entières (dont un multiplieur), une unité mémoire et une FPU.Cette organisation est intéressante, mais encore trop stricte pour donner de bons résultats, les quadruplets avec exactement deux opérations entières, un accès mémoire et une opération flottante étant trop rares. L'usage de ces 4 unités en double ou triple émission a été utilisé sur le PA 7200 et le SuperSPARC. Malheureusement, en plus de problèmes au niveau de son cache, le SuperSPARC avait de nombreuses contraintes d’appariement, car le banc de registre n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions. De plus, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop.
Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, bien plus que quatre, avec au minimum deux ALU entières. Prenons l'exemple de l'UltraSPARC, qui avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplieur flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes, pour de la quadruple émission.
Dans le même genre, les processeurs MIPS R8000 et R10000 avaient une implémentation plus moderne, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités. Moitié opérations entières, moitié opérations mémoire, avec de quoi remplacer les opérations entières par des opérations flottantes. Le PA 8000, lui, dupliquait toutes ses unités en deux exemplaires, ce qui n'est pas l'idéal, aucun processeur moderne ne fait cela aujourd'hui.
Le tout est résumé dans le tableau ci-dessous, avec :
* en vert les CPU utilisant la première stratégie ;
* en rouge les CPU utilisant la seconde stratégie ;
* en jaune les CPU utilisant la troisième stratégie.
{|class="wikitable"
|+ Processeurs quadruple émission historiques
|-
! Année !! Processeur !! Unités
|-
! 1991
| class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques
|-
|-
! rowspan="2" | 1994
| class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'')
|-
| class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM
|-
! rowspan="3" | 1995
| PA 8000 || 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD
|-
| class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques
|-
| MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM
|-
! rowspan="2" | 1996
| class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM
|-
| class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL)
|}
Malheureusement, ces CPUs historiques n'avaient ni assez d'unités de calcul entières, ni d'unités mémoire, pour remplir la quadruple émission.
Environ la moitié des instructions d'un programme correspond à des instructions entières. Pour en profiter, un processeur à N voies doit disposer d'au minimum N/2 ALU entières, l'idéal étant d'aller au-delà. Les CPU superscalaires historiques avaient 2 ALU pour de la quadruple émission, ce qui était très limite. En comparaison, les CPU superscalaires modernes mettent le paquet niveau ALU entière. La plupart des CPU x86 modernes descendent rarement sous les 2/3, la plupart remplit presque la totalité de la largeur du CPU. Pour donner un exemple, la microarchitecture Golden Cove d'Intel est un processeur à 6 voies, avec 5 ALU entières.
Pour être précis, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. La conséquence est que les processeurs modernes dupliquent leurs unités mémoire. Par exemple, la microarchitecture ''Golden Cove'' d'Intel dispose de trois unités mémoire : deux spécialisées dans les écritures, trois dans les lectures. La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire. Notez que le ratio est équilibré entre opérations entières et mémoire, avec soit égalité, soit un avantage pour les ALU entières.
Une autre régularité est que la largeur totale du processeur peut être occupée par des opérations entières/mémoire. Pour le dire autrement, sur un processeur quadruple émission, il pourra émettre un mix de 4 instructions entière/mémoire, avec zéro opération flottante. Idem avec des processeurs triples ou pentuple émission, voire plus. Si le processeur émet une opération flottante, elle remplace une opération entière.
Prenons l'exemple des 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. C'était des processeurs quadruple émission, ce qui fait que la moitié de la largeur du processeur sert pour des opérations entières, et l'autre moitié pour les accès mémoire.
{|class="wikitable"
|-
! Port 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
|}
Les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon, sont un exemple moins représentatif. Ces microarchitectures utilisent le partage des ports d'émission au maximum. C'était des microarchitectures à triple émission, avec six ports d’émission. Il y avait trois ports d'émission pour les opérations flottantes, et trois ports d'émission pour les micro-opérations entières/mémoire. Il y avait trois ALU entières et trois unités mémoire, chaque ALU partageait un port d'émission avec une unité mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements.
{|class="wikitable"
|-
! Port 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 || ALU entière || FADD || FMUL || FPU (autres opérations)
|-
| Multiplieur || || || || ||
|-
| LOAD/STORE || LOAD/STORE || LOAD/STORE || || ||
|}
Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair.
Par exemple, la microarchitecture ''Golden Cove'' a deux ''barrel shifters''. C'est une architecture à 6 voies.
{|class="wikitable"
|-
! Port 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 !! Port d'émission n°7 !! Port d'émission n°8 !! Port d'émission n°9 !! Port d'émission n°10
|-
| ALU entière || ALU entière || ALU entière || ALU entière || ALU entière || LOAD || LOAD || LOAD || STORE || STORE
|-
| ''Barrel shifter'' || Multiplieur || Multiplieur || ''Barrel shifter'' || || || || || ||
|-
| Branchements || Diviseur || || Branchements || || || || || ||
|}
==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===
Dans le cas le plus simple, les accès mémoire sont traités comme des opérations entières un peu spéciales. L'unité mémoire est sur le même port d'émission que l'ALU entière, ce qui fait que les accès mémoire remplacent des opérations entières. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les CPU superscalaire à exécution dans le désordre==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Les unités d'exécution dans le désordre===
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports de ''dispatch''. Mais avec des stations de réservation, il faut le double des ports de ''schedule''. La raison est que les registres sont lus après émission avec une fenê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 de ''schedule'' que de ''dispatch''.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de 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===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Avec plusieurs dizaines d'unités de calcul différentes, ajouter autant 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.
La première solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Le processeur détecte quand il n'y a pas assez de ports pour servir toutes les micro-opérations. Quand ça arrive, l'unité d'émission émet assez de micro-opérations pour exploiter les ports disponibles, mais met en attente les micro-opérations en trop, le temps que les ports se libèrent. Pour cela, le banc de registre est géré par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Une autre solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une dernière solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
1f1wphrdohwcuitd07v8mi5tfib7dfk
Mathc initiation/a522
0
80975
773222
773001
2026-09-26T09:46:11Z
Xhungab
23827
773222
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/a583| Analyse IV : la Transformée en Z : Quelques propriétés]]}}
{{Partie{{{type|}}}|[[Mathc initiation/0075| Analyse IV : la Transformée en Z : Quelques applications]]}}
{{Partie{{{type|}}}|[[Mathc initiation/0079| Analyse IV : la Transformée en Z : Un peu d'entraînement]]}}
{{Partie{{{type|}}}|[[Mathc initiation/0073| Analyse IV : La transformée en Z : Quelques astuces]]}}
{{AutoCat}}
irdq0oasspu82hgv1gdmsd5tpdyvc78
Dictionnaire de philosophie/Anaxagore
0
83143
773218
769336
2026-09-26T06:06:07Z
PandaMystique
119061
773218
wikitext
text/x-wiki
{{DicoPhilo|Anaxagore de Clazomènes|lecture=oui}}
== Vie et contexte historique ==
{{wikisource|Auteur:Anaxagore_de_Clazomènes|Anaxagore de Clazomènes}}
Les données biographiques concernant Anaxagore sont, selon l'expression de Patricia Curd, « confuses et déroutantes » (''confused and confusing''), et la plupart des anecdotes transmises par la tradition antique doivent être abordées avec une prudence critique<ref>Patricia Curd, ''Anaxagoras of Clazomenae: Fragments and Testimonia'', Toronto, University of Toronto Press, 2007, p. 130. Curd rappelle que les chroniqueurs anciens cherchaient moins l'exactitude que les synchronismes et les filiations de maître à disciple.</ref>. Les récits qui nous sont parvenus proviennent pour l'essentiel de sources tardives : Plutarque (I{{er}}-II{{e}} siècle ap. J.-C.) dans ses ''Vies parallèles'', Valère Maxime (I{{er}} siècle ap. J.-C.) dans ses ''Faits et dits mémorables'', puis Diogène Laërce (III{{e}} siècle ap. J.-C.) dans ses ''Vies et doctrines des philosophes illustres''. Ces auteurs compilent des traditions antérieures (Démétrios de Phalère, Apollodore, Sotion, Satyros, Hermippe), parfois stylisées à des fins rhétoriques ou morales, parfois légendaires.
Anaxagore (en grec ancien Ἀναξαγόρας, nom formé sur ἄναξ, « seigneur », et ἀγορά, « assemblée ») naît vers 500 av. J.-C. à Clazomènes, cité grecque d'Ionie située sur la côte occidentale de l'actuelle Turquie, à une trentaine de kilomètres à l'ouest d'Izmir<ref>Diogène Laërce, II, 7 (DK 59 A 1) : selon Apollodore, il naquit lors de la 70{{e}} Olympiade (500-497 av. J.-C.) et mourut la première année de la 88{{e}} (428/7). La date de naissance vers 500 fait l'objet d'un large accord : Curd, 2007, p. 130 ; Malcolm Schofield, ''An Essay on Anaxagoras'', Cambridge, Cambridge University Press, 1980, p. 33 ; David Sider, ''The Fragments of Anaxagoras'', Meisenheim am Glan, Hain, 1981, p. 1-2.</ref>. Fils d'Hégésibule (certaines sources disent Eubule), il appartient, selon la tradition, à une famille aristocratique et fortunée<ref>Diogène Laërce, II, 6 (DK 59 A 1).</ref>. Diogène Laërce rapporte qu'il céda son patrimoine à ses proches et qu'il se consacra à l'étude de la nature en se tenant à l'écart des affaires publiques ; à ceux qui lui reprochaient de négliger ses biens, il aurait répondu : « Pourquoi ne vous en occupez-vous pas vous-mêmes ? »<ref>Diogène Laërce, II, 6-7 (DK 59 A 1). Platon, ''Hippias majeur'', 283a, et Plutarque, ''Vie de Périclès'', 16, évoquent aussi la négligence d'Anaxagore envers son héritage (DK 59 A 13).</ref>. Valère Maxime rapporte de son côté qu'Anaxagore, revenant d'un long voyage et trouvant ses terres à l'abandon, aurait déclaré : « Je ne serais pas sauf si elles n'avaient pas péri »<ref>Valère Maxime, ''Faits et dits mémorables'', VIII, 7, ext. 6 (DK 59 A 31). Diogène Laërce ne rapporte pas ce mot. Valère Maxime y voit une parole empreinte de sagesse.</ref>.
Ces récits, qu'ils soient authentiques ou légendaires, illustrent l'image qu'Anaxagore a laissée : celle du philosophe contemplatif, détaché des affaires domestiques et tourné vers l'étude du cosmos. À quelqu'un qui lui demandait s'il n'avait aucun souci de sa patrie, il aurait répondu, en montrant le ciel : « Tais-toi, j'ai le plus grand souci de ma patrie »<ref>Diogène Laërce, II, 7 (DK 59 A 1).</ref>. Interrogé sur l'homme le plus heureux, il aurait dit qu'aucun de ceux auxquels on songe ne mérite ce titre, et que l'homme heureux paraîtrait étrange à son interlocuteur<ref>Aristote, ''Éthique à Eudème'', I, 4, 1215b6-8 ; cf. ''Éthique à Nicomaque'', X, 9, 1179a13-16 (DK 59 A 30).</ref>. À qui lui demandait pourquoi l'on devrait choisir de naître plutôt que de ne pas être, il aurait répondu : « Pour contempler le ciel et l'ordre de l'univers entier »<ref>Aristote, ''Éthique à Eudème'', I, 5, 1216a11-14 (DK 59 A 30). Schofield (1980, p. 22-23) rapproche ces anecdotes du portrait platonicien du philosophe dans la digression du ''Théétète'' et en souligne le caractère stylisé.</ref>.
=== L'arrivée à Athènes et l'activité philosophique ===
La chronologie de la vie d'Anaxagore reste discutée, en raison des divergences entre les sources anciennes<ref>Leonard Woodbury, « Anaxagoras and Athens », ''Phoenix'', vol. 35, 1981, p. 295-315 ; Jaap Mansfeld, « The Chronology of Anaxagoras' Athenian Period and the Date of His Trial », ''Mnemosyne'', vol. 32, 1979, p. 39-69, et vol. 33, 1980, p. 17-95 ; Schofield, 1980, p. 33-35 ; Sider, 1981, p. 1-11.</ref>. Diogène Laërce rapporte, d'après Démétrios de Phalère, qu'Anaxagore commença à philosopher à Athènes à l'âge de vingt ans, sous l'archontat de Callias, et qu'il y séjourna trente ans ; il le dit aussi âgé de vingt ans lors du passage de Xerxès (480 av. J.-C.)<ref>Diogène Laërce, II, 7 (DK 59 A 1). Le nom de l'archonte pose problème : Callias correspond à 456/5, tandis que la correction « Calliadès » (480/79) s'accorde avec l'âge de vingt ans. Voir Sider, 1981, p. 2-4 ; Curd, 2007, p. 78, n. 1, et p. 131.</ref>. Une arrivée précoce, vers 480, est défendue par Denis O'Brien, Leonard Woodbury et Daniel Graham ; Jaap Mansfeld, que suit Curd, retient 456/5 et un séjour de vingt ans ; d'autres proposent des dates intermédiaires<ref>Denis O'Brien, « The Relation of Anaxagoras and Empedocles », ''Journal of Hellenic Studies'', vol. 88, 1968, p. 93-113 ; Woodbury, 1981 ; Daniel W. Graham, ''Explaining the Cosmos: The Ionian Tradition of Scientific Philosophy'', Princeton, Princeton University Press, 2006 ; Mansfeld, 1979 ; Curd, 2007, p. 131. Pour une présentation récente des hypothèses (480, 478, 464, 460, 456), voir Patricia Curd et John Sisko, « Anaxagoras », ''Stanford Encyclopedia of Philosophy'', 2007, révision substantielle 2026, § 1.</ref>. Les sources attribuent au séjour athénien une durée de vingt ou de trente ans ; selon la chronologie retenue, on le situe approximativement entre 480 et 450 ou entre 456 et 437 av. J.-C. Schofield, de son côté, place la composition du livre vers 470-460 av. J.-C.<ref>Curd, 2007, p. 129 ; Schofield, 1980, p. 33-35.</ref>.
Quelle que soit la date de son arrivée, Anaxagore est présenté par la tradition comme le premier philosophe à s'être établi à Athènes et à y avoir apporté la recherche ionienne sur la nature<ref>Clément d'Alexandrie, ''Stromates'', I, 63 (DK 59 A 7) : Anaxagore aurait transporté d'Ionie à Athènes l'enseignement d'Anaximène. La filiation avec Anaximène est chronologiquement peu vraisemblable (Curd, 2007, p. 129, n. 1).</ref>. Athènes, après ses victoires sur les Perses à Marathon (490) et à Salamine (480), devient alors le principal centre politique et intellectuel du monde grec. C'est dans ce contexte que la tradition situe l'enseignement d'Anaxagore.
La tradition biographique, dominée par le récit de Plutarque, lie Anaxagore à Périclès<ref>Plutarque, ''Vie de Périclès'', 4-6, 16, 32 (DK 59 A 15-17) ; Platon, ''Phèdre'', 269e-270a (DK 59 A 15) ; Isocrate, ''Sur l'échange'', 235 (DK 59 A 15), qui fait de Périclès le disciple d'Anaxagore et de Damon ; Diodore de Sicile, XII, 39 (DK 59 A 17), qui appelle Anaxagore « le maître de Périclès ».</ref>. Curd juge cette amitié « bien établie », mais Schofield invite à ne pas prendre trop au sérieux le titre d'« élève » donné à Périclès : il en retient seulement la vraisemblance d'une influence exercée dans la jeunesse de l'homme d'État<ref>Curd, 2007, p. 132 ; Schofield, 1980, p. 34. Sider (1981, p. 3) rappelle que ce genre de récit reflète souvent la seule conviction qu'un homme plus jeune a appris de son aîné.</ref>. L'image d'un philosophe naturaliste qui aurait donné au grand orateur démocratique son éloquence élevée provient d'abord du ''Phèdre'' de Platon ; elle appartient en partie à la construction biographique et ne doit pas être surinterprétée.
Parmi les auditeurs d'Anaxagore, la tradition mentionne le poète Euripide<ref>Diogène Laërce, II, 10 (DK 59 A 1) ; Alexandre d'Étolie, cité par Aulu-Gelle, ''Nuits attiques'', XV, 20 (DK 59 A 21). Curd (2007, p. 132-133, n. 13) souligne que la présence d'échos anaxagoréens chez Euripide n'implique pas une relation formelle de maître à élève.</ref> et Archélaos, qui aurait ensuite été le maître de Socrate<ref>Diogène Laërce, II, 16 (DK 60 A 1) ; Curd, 2007, p. 134 et n. 18.</ref>. La question de savoir si Socrate a rencontré Anaxagore reste ouverte. Dans le ''Phédon'', Socrate dit avoir entendu quelqu'un lire le livre d'Anaxagore, puis l'avoir lu lui-même<ref>Platon, ''Phédon'', 97b-99d (DK 59 A 47). Platon ne nomme pas le lecteur ; l'hypothèse selon laquelle il s'agirait d'Archélaos est une conjecture moderne (Curd, 2007, p. 134 ; Burnet la formule avec un point d'interrogation).</ref>. Certains en ont conclu qu'Anaxagore avait quitté Athènes avant que Socrate ne s'intéresse à la philosophie ; Curd juge cet argument du silence peu probant<ref>Curd, 2007, p. 134-136 ; en sens contraire, Woodbury, 1981, et Schofield, 1980, p. 34-35.</ref>. Dans l’''Apologie'', Socrate rappelle que les livres d'Anaxagore pouvaient s'acquérir pour une drachme au plus « à l'orchestra »<ref>Platon, ''Apologie de Socrate'', 26d-e (DK 59 A 35). Sur l'interprétation de ce passage (achat de livres ou de lectures), voir Curd, 2007, p. 92, n. 12.</ref>, ce qui atteste la diffusion de ses idées à Athènes à la fin du V{{e}} siècle.
=== L'œuvre écrite ===
Anaxagore est l'auteur d'un seul ouvrage en prose ionienne, que Simplicius désigne comme un traité ''Sur la nature'' (Περὶ φύσεως) et dont il cite le « premier livre »<ref>Diogène Laërce, I, 16 (DK 59 A 37), range Anaxagore parmi les auteurs d'un seul livre ; Simplicius, ''Commentaire sur la Physique d'Aristote'', 34, 29 et 155, 26.</ref>. Platon emploie le pluriel τὰ Ἀναξαγόρου βιβλία dans l’''Apologie''<ref>Platon, ''Apologie'', 26d.</ref> : Burnet y voyait l'indice d'un ouvrage occupant plusieurs rouleaux, ce que Raven contestait<ref>John Burnet, ''Early Greek Philosophy'', 4{{e}} éd., Londres, A. & C. Black, 1930, chap. VI ; Sven-Tage Teodorsson, ''Anaxagoras' Theory of Matter'', Göteborg, Acta Universitatis Gothoburgensis, 1982, p. 10.</ref>. Les écrits qu'on lui attribue par ailleurs (sur la quadrature du cercle, sur la perspective, un recueil de problèmes) sont presque certainement apocryphes<ref>DK 59 A 38-40 ; Teodorsson, 1982, p. 10 ; Burnet, 1930, chap. VI.</ref>. Diogène Laërce rapporte encore qu'Anaxagore fut le premier à publier un livre accompagné de figures<ref>Diogène Laërce, II, 11 (DK 59 A 1).</ref>.
Diogène Laërce qualifie son style d'agréable et d'élevé<ref>Diogène Laërce, II, 6 (DK 59 A 1).</ref>. Schofield a analysé la texture de cette prose archaïque : syntaxe paratactique, répétitions, et, dans le fragment B12, un style de « prédication solennelle » qui accumule les attributs du Noûs à la manière d'un hymne<ref>Schofield, 1980, p. 6-9, qui s'appuie sur Karl Deichgräber, « Hymnische Elemente in der philosophischen Prosa der Vorsokratiker », ''Philologus'', vol. 88, 1933, p. 347-361, et renvoie à Eduard Norden et Hermann Fränkel.</ref>. Contrairement à Parménide et à Empédocle, qui écrivent en hexamètres, Anaxagore s'inscrit dans la tradition ionienne de la prose, inaugurée par Anaximandre et Anaximène. Il subsiste de ce traité une vingtaine de fragments, conservés pour la plupart par Simplicius de Cilicie (VI{{e}} siècle ap. J.-C.) dans ses commentaires sur Aristote<ref>Édition de référence : Hermann Diels et Walther Kranz, ''Die Fragmente der Vorsokratiker'', 6{{e}} éd., Berlin, Weidmann, 1951-1952, vol. II, p. 5-44 (chapitre 59, témoignages A et fragments B). Édition plus récente, avec une nouvelle numérotation (Anaxagore y porte le numéro 25) : André Laks et Glenn W. Most, ''Early Greek Philosophy'', 9 vol., Cambridge (Mass.), Harvard University Press, 2016 ; version française : ''Les Débuts de la philosophie'', Paris, Fayard, 2016. Voir aussi Curd, 2007, et Sider, 1981 (2{{e}} éd. 2005).</ref>. Les citations de Simplicius semblent limitées au premier livre, consacré aux principes généraux<ref>Burnet, 1930, chap. VI ; Teodorsson, 1982, p. 10. Schofield (1980, p. 159, n. 36) doute que Simplicius ait disposé du livre entier plutôt que d'extraits (cf. Curd, 2007, p. 165, n. 25).</ref>.
=== Le procès et l'exil ===
Les circonstances du départ d'Anaxagore d'Athènes sont particulièrement controversées. Diogène Laërce rapporte plusieurs versions de son procès<ref>Diogène Laërce, II, 12-14 (DK 59 A 1).</ref>. Selon Sotion, Anaxagore fut accusé d'impiété (ἀσέβεια) par Cléon pour avoir soutenu que le soleil était une masse de métal incandescent ; défendu par Périclès, il fut condamné à une amende de cinq talents et à l'exil<ref>Sotion, dans Diogène Laërce, II, 12.</ref>. Selon Satyros, l'accusateur fut Thucydide, fils de Mélésias, adversaire politique de Périclès, et l'accusation porta sur l'impiété et le médisme ; Anaxagore aurait été condamné à mort par contumace<ref>Satyros, dans Diogène Laërce, II, 12.</ref>. Hermippe le dit emprisonné en attente d'exécution puis libéré à la demande de Périclès ; Hiéronymos rapporte que Périclès le présenta au tribunal affaibli par la maladie, si bien qu'il fut acquitté par pitié<ref>Hermippe et Hiéronymos, dans Diogène Laërce, II, 13-14.</ref>.
Plutarque rapporte qu'un certain Diopeithès fit voter un décret autorisant les poursuites contre ceux qui ne reconnaissaient pas les dieux ou enseignaient des doctrines sur les phénomènes célestes, afin de jeter le soupçon sur Périclès à travers Anaxagore ; Périclès, craignant pour Anaxagore, l'éloigna de la cité<ref>Plutarque, ''Vie de Périclès'', 32, 1-5 (DK 59 A 17). Plutarque ne précise pas le contenu des doctrines incriminées. La thèse du soleil-pierre et de la lune-terre est mentionnée par Platon, ''Apologie'', 26d (DK 59 A 35), où Mélétos attribue ces opinions à Socrate. Plutarque (''Vie de Nicias'', 23 = DK 59 A 18) dit aussi que Périclès eut peine à tirer Anaxagore de prison.</ref>.
L'historicité du procès a été mise en doute, notamment par K. J. Dover, pour qui aucune des sources anciennes ne savait réellement ce qui était arrivé à Anaxagore ; J. A. Davison a au contraire tenté de reconstituer les événements, en supposant deux séjours athéniens<ref>K. J. Dover, « The Freedom of the Intellectual in Greek Society », ''Talanta'', vol. 7, 1975, p. 24-54, cité par Schofield, 1980, p. 34 et n. 74 ; J. A. Davison, « Protagoras, Democritus, and Anaxagoras », ''Classical Quarterly'', n.s. vol. 3, 1953, p. 33-45 (cf. Teodorsson, 1982, p. 9). Gershenson et Greenberg, dans ''Anaxagoras and the Birth of Physics'' (1964), nient également le procès (Teodorsson, 1982, p. 7, n. 2).</ref>. Curd estime néanmoins qu'il y a peu de raisons de douter de la tradition sur ce point<ref>Curd, 2007, p. 136.</ref>. La date reste incertaine : vers 450 selon A. E. Taylor et Burnet, en 437/6 selon Mansfeld, vers 434 selon Sider, vers 430 selon Richard Janko<ref>A. E. Taylor, « On the Date of the Trial of Anaxagoras », ''Classical Quarterly'', vol. 11, 1917, p. 81-87 ; Burnet, 1930, chap. VI ; Mansfeld, 1979-1980 ; Sider, 1981, p. 1-11 ; Richard Janko, « Eclipse and Plague: Themistocles, Pericles, Anaxagoras and the Athenians' War on Science », ''Journal of Hellenic Studies'', vol. 140, 2020, p. 213-237. Pour la synthèse, voir Curd et Sisko, ''SEP'', 2026, § 1.</ref>.
Anaxagore se retira ensuite à Lampsaque, sur l'Hellespont (l'actuel détroit des Dardanelles), où il fut honoré<ref>Diogène Laërce, II, 14-15 (DK 59 A 1) ; Alcidamas, cité par Aristote, ''Rhétorique'', II, 23, 1398b15-16 (DK 59 A 23).</ref>. Il y mourut en 428 av. J.-C., à soixante-douze ans selon Apollodore<ref>Diogène Laërce, II, 7 (DK 59 A 1).</ref>. Invité par les magistrats de la cité à exprimer un vœu, il aurait demandé que les enfants aient congé chaque année dans le mois de sa mort, usage encore observé au temps de Diogène Laërce<ref>Diogène Laërce, II, 14 (DK 59 A 1).</ref>. Élien rapporte qu'un autel lui fut élevé, portant les inscriptions « Intellect » (Νοῦς) et « Vérité » (Ἀλήθεια)<ref>Élien, ''Histoire variée'', VIII, 19 (DK 59 A 24). Le texte peut signifier soit un autel portant une inscription sur chaque face, soit un autel dédié aux deux (Curd, 2007, p. 88, n. 10).</ref>. L'épitaphe gravée sur sa tombe nous est parvenue : « Ci-gît Anaxagore, qui atteignit le plus lointain terme de la vérité sur le cosmos céleste »<ref>Diogène Laërce, II, 15 (DK 59 A 1) ; Élien, ''Histoire variée'', VIII, 19 (DK 59 A 24). Alcidamas, au IV{{e}} siècle, atteste que les Lampsacéniens l'honoraient encore (DK 59 A 23) ; Gershenson et Greenberg (1964, p. 4) rappellent que sa mémoire fut honorée pendant plus d'un siècle.</ref>. Cicéron rapporte enfin qu'à des amis qui lui demandaient, à Lampsaque, s'il voulait être ramené à Clazomènes, il aurait répondu que le chemin des Enfers est le même de partout<ref>Cicéron, ''Tusculanes'', I, 43, 104 (DK 59 A 34a) ; cf. Diogène Laërce, II, 11.</ref>.
=== Portrait et anecdotes ===
Les sources anciennes conservent de nombreuses anecdotes qui, sans garantie historique, témoignent de l'image du philosophe dans la tradition. Apprenant la mort de son fils, Anaxagore aurait dit avec le plus grand calme : « Je savais que j'avais engendré un mortel »<ref>Galien, ''Des opinions d'Hippocrate et de Platon'', IV, 7 (DK 59 A 33) ; Diogène Laërce, II, 13, rapporte le mot au pluriel, à propos de ses enfants.</ref>. Ces récits ont fait de lui, dans la tradition, le modèle du sage détaché des contingences et consacré à la contemplation de la nature.
Dans le ''Phèdre'', Socrate attribue l'élévation de l'éloquence de Périclès à sa fréquentation d'Anaxagore, dont les discours portaient pour l'essentiel sur la nature de l'intelligence et de la déraison (νοῦ τε καὶ ἀνοίας)<ref>Platon, ''Phèdre'', 269e-270a (DK 59 A 15) ; Schofield, 1980, p. 22-23.</ref>. Cette remarque témoigne de la réputation d'Anaxagore dans l'Athènes classique : celle d'un penseur dont les spéculations « météorologiques » (au sens ancien de l'étude des phénomènes célestes) pouvaient paraître étranges au commun. Les ''Nuées'' d'Aristophane tournent en ridicule la nouvelle science de la nature ; toutefois, selon Dover, que suit Schofield, les idées moquées dans la pièce se rattachent plus aisément à Diogène d'Apollonie qu'à Anaxagore<ref>Aristophane, ''Les Nuées'' ; Kenneth Dover, ''Aristophanes: Clouds'', Oxford, Clarendon Press, 1968, introduction ; Schofield, 1980, p. 35 et n. 83 ; Curd, 2007, p. 132, n. 11. Gábor Betegh a proposé de voir aussi dans la pièce des échos d'Archélaos (Betegh, 2016).</ref>.
=== L'école de Lampsaque ===
Anaxagore semble avoir poursuivi son activité à Lampsaque et y avoir fondé une école prospère<ref>Schofield, 1980, p. 35 et n. 84, qui s'appuie sur Diogène Laërce, II, 14-15, Aristote, ''Rhétorique'', 1398b15 (DK 59 A 23), et Eusèbe, ''Préparation évangélique'', X, 14, 13 (DK 59 A 7) ; Sider, 1981, p. 3-4 ; Burnet, 1930, chap. VI. Curd (2007, p. 129, n. 1) rappelle que nous ignorons la nature exacte de son activité philosophique, et notamment s'il eut une école.</ref>. Eusèbe rapporte qu'Archélaos lui succéda à la tête de cette école de Lampsaque<ref>Eusèbe, ''Préparation évangélique'', X, 14, 13 (DK 59 A 7). Cette indication s'accorde mal avec les témoignages qui situent Archélaos à Athènes ; Curd (2007, p. 134, n. 18) note que la tradition a pu confondre certains aspects de la vie des deux hommes.</ref>. Parmi ses proches, les sources mentionnent aussi Métrodore de Lampsaque, qui prolongea l'interprétation d'Homère attribuée à Anaxagore en étudiant la « physique » du poète<ref>Diogène Laërce, II, 11 (DK 59 A 1), d'après Favorinus ; DK 61. Schofield (1980, p. 149, n. 59) range Archélaos (DK 60 A 1-5) et Métrodore (DK 61 A 2, 6) parmi ses disciples.</ref>. Cette présence à Lampsaque contribue sans doute à expliquer que ses idées aient continué de circuler dans le monde grec après son départ d'Athènes.
== Les principes métaphysiques ==
La philosophie d'Anaxagore se construit pour une large part en réponse aux exigences posées par Parménide d'Élée<ref>Curd, 2007, p. 137-142 ; Patricia Curd, ''The Legacy of Parmenides: Eleatic Monism and Later Presocratic Thought'', Princeton, Princeton University Press, 1998 (rééd. Las Vegas, Parmenides Publishing, 2004) ; Schofield, 1980, p. 5.</ref>. Dans son poème, Parménide oppose deux voies de recherche : celle « qu'il est et qu'il n'est pas possible qu'il ne soit pas » (ὅπως ἔστιν τε καὶ ὡς οὐκ ἔστι μὴ εἶναι) et celle « qu'il n'est pas et qu'il faut qu'il ne soit pas »<ref>Parménide, DK 28 B 2, 3-5 ; cf. B 6, 1-2 (ἔστι γὰρ εἶναι, μηδὲν δ' οὐκ ἔστιν, « car être est, et le néant n'est pas »).</ref>. Il en conclut que toute génération et toute corruption véritables sont impossibles, car elles supposeraient un passage de ce qui n'est pas à ce qui est, ou l'inverse<ref>Parménide, B 8, 6-21 ; cf. Aristote, ''Physique'', I, 8, 191a23-31.</ref>. Ce qui est véritablement doit en outre être un, continu, homogène et immobile<ref>Parménide, B 8, 22 : « Il n'est pas divisible, puisqu'il est tout entier semblable » (οὐδὲ διαιρετόν ἐστιν, ἐπεὶ πᾶν ἐστιν ὁμοῖον).</ref>.
Anaxagore accepte l'impossibilité de la génération à partir de ce qui n'est pas, mais refuse d'en conclure que la pluralité et le changement sont illusoires<ref>Aristote, ''Métaphysique'', A, 3, 984a11-16 (DK 59 A 43) ; ''Physique'', I, 4, 187a26-29 (DK 59 A 52).</ref>. Théophraste, cité par Simplicius, dit qu'Anaxagore, qui avait partagé la philosophie d'Anaximène, fut le premier à modifier les doctrines sur les principes et à fournir la cause qui leur manquait<ref>Théophraste, dans Simplicius, ''Commentaire sur la Physique'', 27, 2-4 (DK 59 A 41). La « cause qui manquait » désigne vraisemblablement le Noûs, cause du mouvement (Curd, 2007, p. 93, n. 13). Le lien avec Anaximène doit s'entendre au sens large d'une appartenance à la tradition ionienne (Curd, 2007, p. 129, n. 1 ; Burnet, 1930, chap. VI).</ref>. Plutôt que de nier le devenir, Anaxagore cherche à fonder une cosmologie qui satisfasse les exigences éléates tout en rendant compte de la multiplicité et du mouvement observables. Schofield souligne toutefois qu'il reste un cosmologue ionien, qui intègre certaines thèses de Parménide sans adopter sa méthode argumentative<ref>Schofield, 1980, p. 5 et 26-28 ; G. E. L. Owen, « Eleatic Questions », ''Classical Quarterly'', n.s. vol. 10, 1960, p. 84-102. Sur le débat entre ceux qui voient en Anaxagore un critique de Parménide et ceux qui en font un disciple, voir Curd et Sisko, ''SEP'', 2026, § 2.</ref>. G. E. L. Owen a relevé que la première phrase du traité, « Toutes choses étaient ensemble », est « manifestement formulée comme une contradiction plate de Parménide » sur plusieurs points essentiels<ref>G. E. L. Owen, « Plato and Parmenides on the Timeless Present », ''The Monist'', vol. 50, 1966, p. 317-340, repris dans A. P. D. Mourelatos (éd.), ''The Pre-Socratics'', Garden City, Anchor Press, 1974, p. 271-292, ici p. 276-277 ; cité par Schofield, 1980, p. 64 et n. 51.</ref>.
=== Le principe de conservation ===
Le premier principe d'Anaxagore est énoncé dans le fragment B17 : « Les Grecs ne pensent pas correctement la naissance et la destruction : aucune chose ne naît ni ne périt, mais, à partir des choses qui sont, il y a mélange et dissociation. Ainsi auraient-ils raison d'appeler la naissance “mélange” et la destruction “dissociation” » (τὸ γίνεσθαι συμμίσγεσθαι καὶ τὸ ἀπόλλυσθαι διακρίνεσθαι)<ref>Anaxagore, fragment B17, cité par Simplicius, ''Commentaire sur la Physique'', 163, 18-24 (DK 59 B 17 ; LM 25 D15).</ref>. Ce principe constitue la base de sa physique et sa réponse à l'interdit parménidien. Ce qui apparaît comme génération n'est qu'un réarrangement d'ingrédients préexistants qui se mélangent (συμμίσγεται) ; ce qui paraît destruction n'est que leur dissociation (διακρίνεται)<ref>Aristote, ''Métaphysique'', A, 3, 984a13-16 (DK 59 A 43) : selon Anaxagore, presque tous les homéomères naissent et périssent seulement par agrégation et dissociation. Curd (2007, p. 145) rappelle que l'assimilation de la génération à l'altération, qu'Aristote prête à Anaxagore (''Génération et corruption'', I, 1, 314a11-13 = DK 59 A 52), est une interprétation aristotélicienne.</ref>. Rien ne naît du néant, rien n'y retourne : les ingrédients de la réalité existent de toute éternité et conservent leur nature propre.
Aristote explique la position d'Anaxagore par son adhésion à l'opinion commune des physiciens selon laquelle rien ne naît de ce qui n'est pas<ref>Aristote, ''Physique'', I, 4, 187a26-29 (DK 59 A 52) ; Schofield, 1980, p. 43-44.</ref>. Le principe prend une forme plus précise dans une question que la tradition attribue à Anaxagore : « Comment le cheveu pourrait-il naître de ce qui n'est pas cheveu, et la chair de ce qui n'est pas chair ? » (πῶς γὰρ ἂν ἐκ μὴ τριχὸς γένοιτο θρὶξ καὶ σὰρξ ἐκ μὴ σαρκός;)<ref>Anaxagore, fragment B10, transmis par une scholie à Grégoire de Nazianze (DK 59 B 10). L'authenticité de la formule est discutée : Schofield l'a d'abord contestée (« Doxographica Anaxagorea », ''Hermes'', vol. 103, 1975, p. 1-24), puis a admis que le scholiaste, dont la source pourrait être Eudème, en conserve peut-être les termes (Schofield, 1980, p. 133-143). La plupart des spécialistes la tiennent pour authentique (Sider ; Curd, 2007, p. 53-54).</ref>. Cette formule exprime ce que les commentateurs appellent le principe « du semblable par le semblable » : une substance ne peut provenir que de la même substance, déjà présente sous une forme non manifeste<ref>Schofield, 1980, p. 44 et 55-58, qui analyse ce principe dans la reconstruction aristotélicienne.</ref>.
On rapproche parfois ce principe de la loi de conservation de la matière formulée par Antoine-Laurent de Lavoisier et de la maxime « Rien ne se perd, rien ne se crée, tout se transforme ». L'analogie ne doit pas être surinterprétée : les mécanismes qu'Anaxagore propose pour expliquer les transformations (mélange et dissociation d'ingrédients éternels) sont qualitatifs et ne relèvent pas d'une chimie quantitative au sens moderne<ref>Antoine-Laurent de Lavoisier, ''Traité élémentaire de chimie'', Paris, Cuchet, 1789, t. I, chap. XIII, où est posé le principe d'une égale quantité de matière avant et après toute opération. Le rapprochement avec Anaxagore relève de l'analogie rétrospective, non d'une filiation historique.</ref>. L'intuition d'Anaxagore partage avec le principe de Lavoisier l'idée que rien ne se perd ni ne se crée dans le devenir physique ; il serait anachronique d'y voir davantage.
=== Tout est dans tout ===
Le deuxième principe s'énonce ainsi : « En toute chose il y a une part de toute chose, sauf de l'Intellect ; et il est des choses dans lesquelles l'Intellect aussi est présent » (ἐν παντὶ παντὸς μοῖρα ἔνεστι πλὴν νοῦ, ἔστιν οἷσι δὲ καὶ νοῦς ἔνι)<ref>Anaxagore, fragment B11, cité par Simplicius, ''Commentaire sur la Physique'', 164, 23-24 (DK 59 B 11). La même thèse figure en B6 et au début de B12.</ref>. Ce principe, souvent désigné par la formule latine ''omnia in omnibus'', est la thèse la plus caractéristique et la plus déroutante de la philosophie d'Anaxagore ; Curd le nomme « principe du mélange universel »<ref>Curd, 2007, p. 179 et n. 3 ; cf. Gregory Vlastos, « The Physical Theory of Anaxagoras », ''Philosophical Review'', vol. 59, 1950, p. 31-57 ; Colin Strang, « The Physical Theory of Anaxagoras », ''Archiv für Geschichte der Philosophie'', vol. 45, 1963, p. 101-118.</ref>. Il signifie que toute portion de matière, si petite soit-elle, contient des parts de tous les ingrédients qui existent : aucun ingrédient n'existe à l'état pur, isolé de tous les autres<ref>Anaxagore, B6 : « Puisqu'il n'est pas possible qu'il y ait un plus petit, rien ne pourrait être séparé ni venir à être par soi-même, mais, comme au commencement, maintenant aussi toutes choses sont ensemble » (ὅτε τοὐλάχιστον μὴ ἔστιν εἶναι, οὐκ ἂν δύναιτο χωρισθῆναι, οὐδ' ἂν ἐφ' ἑαυτοῦ γενέσθαι, ἀλλ' ὅπωσπερ ἀρχὴν εἶναι καὶ νῦν πάντα ὁμοῦ).</ref>. Ce qui nous apparaît comme de l'or contient, outre l'or qui y prédomine, des parts de tous les autres ingrédients<ref>Théophraste, dans Simplicius, ''Commentaire sur la Physique'', 27, 2-11 (DK 59 A 41) ; Aristote, ''Physique'', I, 4, 187a36-b7.</ref>.
La tradition doxographique rattache cette thèse aux phénomènes de la nutrition et de la croissance. Comment la chair pourrait-elle provenir du pain et de l'eau que nous consommons, si le pain et l'eau ne contenaient pas déjà de la chair ? La nourriture doit donc contenir, de manière imperceptible, toutes les substances qui composent le corps<ref>Aétius, I, 3, 5 (DK 59 A 46) ; Simplicius, ''Commentaire sur la Physique'', 460, 4-20 (DK 59 A 45). La scholie qui transmet B10 affirme que la semence contient cheveux, ongles, veines, artères, nerfs et os, imperceptibles en raison de leur petitesse et qui se séparent peu à peu au cours de la croissance.</ref>. Curd et Schofield soulignent cependant que la nutrition n'est qu'un cas, particulièrement frappant, d'une question métaphysique plus générale sur le devenir, héritée de Parménide<ref>Curd, 2007, p. 179-180 ; Schofield, 1980, p. 121 et 133-143.</ref>.
Le principe vaut aussi pour les qualités opposées. Selon la scholie à Grégoire de Nazianze, Anaxagore affirmait qu'il y a du noir dans le blanc et du blanc dans le noir, et du léger dans le lourd<ref>Scholie à Grégoire de Nazianze, contexte de DK 59 B 10 (texte et traduction dans Schofield, 1980, p. 135-136). Sextus Empiricus (''Hypotyposes pyrrhoniennes'', I, 33 = DK 59 A 97) rapporte qu'Anaxagore disait la neige noire, puisqu'elle est de l'eau congelée et que l'eau est noire.</ref>. Ces oppositions ne sont donc pas absolues : ce qui nous paraît blanc contient du noir, en proportion trop faible pour être perçue.
Cette doctrine suscite des débats depuis l'Antiquité. Aristote y voit la conséquence de l'observation selon laquelle n'importe quoi naît de n'importe quoi<ref>Aristote, ''Physique'', III, 4, 203a23-33 (DK 59 A 45) ; Schofield, 1980, p. 43-52.</ref>. Les interprètes modernes se divisent sur la nature des « parts » (μοῖραι). Pour les uns, il s'agit de particules infiniment petites (lecture déjà présente chez Lucrèce) ; pour les autres, de proportions ou de concentrations variables d'ingrédients qui se compénètrent sans structure corpusculaire<ref>Lecture particulaire : Lucrèce, ''De la nature'', I, 830-920 (DK 59 A 44) ; Vlastos, 1950 ; W. K. C. Guthrie, ''A History of Greek Philosophy'', vol. II, Cambridge, Cambridge University Press, 1965, p. 289 ; George B. Kerferd, « Anaxagoras and the Concept of Matter before Aristotle », ''Bulletin of the John Rylands Library'', vol. 52, 1969, p. 129-143 ; Sider, 1981. Lecture non particulaire : Schofield, 1980, p. 73-79 ; Jonathan Barnes, ''The Presocratic Philosophers'', Londres, Routledge, 1982, p. 323-326 ; Brad Inwood, « Anaxagoras and Infinite Divisibility », ''Illinois Classical Studies'', vol. 11, 1986, p. 17-33, ici p. 19-22 ; Curd, 2007, p. 183-184 ; Anna Marmodoro, « Anaxagoras's Qualitative Gunk », ''British Journal for the History of Philosophy'', vol. 23, 2015, p. 402-422, et ''Everything in Everything: Anaxagoras's Metaphysics'', New York, Oxford University Press, 2017, chap. 3-4. Voir le recensement de Curd, 2007, p. 183, n. 9-10.</ref>. Quoi qu'il en soit, ce principe forme la clé de voûte de sa physique.
=== Pas de plus petit ni de plus grand ===
Le troisième principe est exposé dans le fragment B3 : « Car du petit il n'y a pas de plus petit, mais toujours un plus petit (car ce qui est ne peut pas ne pas être) ; mais du grand aussi il y a toujours un plus grand, et il est égal au petit en quantité ; et, rapportée à elle-même, chaque chose est à la fois grande et petite » (οὔτε γὰρ τοῦ σμικροῦ ἐστί τό γε ἐλάχιστον, ἀλλ' ἔλασσον ἀεί· τὸ γὰρ ἐὸν οὐκ ἔστι τὸ μὴ οὐκ εἶναι)<ref>Anaxagore, fragment B3, cité par Simplicius, ''Commentaire sur la Physique'', 164, 17-20 (DK 59 B 3). Zeller a proposé de corriger τὸ μή des manuscrits en τομῇ (« par division ») : la parenthèse signifierait alors que ce qui est ne peut cesser d'être par division. Burnet et Sider adoptent la correction ; Curd défend le texte des manuscrits (Curd, 2007, p. 39-40 ; Schofield, 1980, p. 156-157, n. 15).</ref>. Ce principe exclut l'existence d'un plus petit et d'un plus grand, et s'oppose à toute conception atomiste<ref>Leucippe et Démocrite, DK 67-68 ; Aristote, ''Génération et corruption'', I, 2, 315b28-317a2, et I, 8, 325a23-b5.</ref>.
Contrairement à Leucippe et à Démocrite, qui posent des corps insécables (ἄτομα), Anaxagore soutient qu'aucune portion de matière n'est minimale et que toute portion, si petite soit-elle, contient encore toutes choses<ref>Anaxagore, B6 : « Puisque les parts du grand et du petit sont égales en quantité, de cette manière aussi toutes choses seraient en tout » (καὶ ὅτε δὲ ἴσαι μοῖραί εἰσι τοῦ τε μεγάλου καὶ τοῦ σμικροῦ πλῆθος, καὶ οὕτως ἂν εἴη ἐν παντὶ πάντα).</ref>. Cette infinité dans la petitesse se double d'une infinité dans la grandeur : il n'existe pas plus de limite supérieure que de limite inférieure. Cette double infinité pose des problèmes interprétatifs importants, tant aux commentateurs anciens qu'aux modernes<ref>Montgomery Furth, « A “Philosophical Hero”? Anaxagoras and the Eleatics », ''Oxford Studies in Ancient Philosophy'', vol. 9, 1991, p. 95-129 ; David J. Furley, « Anaxagoras, Plato and the Naming of Parts », dans Victor Caston et Daniel W. Graham (éd.), ''Presocratic Philosophy: Essays in Honour of Alexander Mourelatos'', Aldershot, Ashgate, 2002, p. 119-126 ; Marmodoro, 2017, chap. 2-3.</ref>.
Le principe de non-minimum a une conséquence importante : le mélange universel ne pourra jamais être défait. Puisqu'il n'existe pas de plus petite quantité d'un ingrédient, celui-ci ne peut jamais être entièrement extrait d'un mélange ; sa proportion peut diminuer indéfiniment sans jamais devenir nulle<ref>Anaxagore, B6 ; Curd, 2007, p. 182-185. Simplicius commente B3 en ces termes : si tout est en tout et si tout se sépare de tout, alors, de ce qui semble le plus petit, se séparera encore quelque chose de plus petit, et ce qui semble le plus grand s'est séparé de quelque chose de plus grand (''Commentaire sur la Physique'', 164, 20-23).</ref>. Comme le formule Malcolm Schofield à la suite de Colin Strang, la complexité de la composition n'est pas fonction de la taille<ref>Schofield, 1980, p. 69, 79 et 90 ; Strang, 1963, repris dans David J. Furley et R. E. Allen (éd.), ''Studies in Presocratic Philosophy'', vol. II, Londres, Routledge and Kegan Paul, 1975, p. 361-380, ici p. 366 ; cf. Curd, 2007, p. 40 et 50, n. 35.</ref>.
La justification donnée en B3 reprend l'axiome parménidien : « car ce qui est ne peut pas ne pas être ». Si l'on pouvait diviser un ingrédient jusqu'à le faire disparaître, il y aurait passage de l'être au non-être, ce qu'interdit Parménide. Toute division, aussi poussée soit-elle, laisse subsister quelque chose, et ce reste contient encore des parts de tous les ingrédients<ref>Curd, 2007, p. 39-40 et 184-185.</ref>.
=== Le principe de prédominance ===
Du principe « tout est dans tout » découle une difficulté : si chaque chose contient une part de toutes les autres, comment expliquer que nous percevions des objets distincts ? Anaxagore la résout par ce que les commentateurs modernes appellent le « principe de prédominance », expression qui n'est pas la sienne<ref>Schofield, 1980, p. 87 (où il cite les « principles of latency and predominance » de David Furley) et p. 108-111 ; Curd, 2007, p. 188-189 ; Marmodoro (2017, chap. 2) parle de ''preponderance principle''. Les commentateurs néoplatoniciens formulaient déjà les principes du « tout en tout » et de la prédominance (Schofield, 1980, p. 155, n. 52).</ref>. La fin du fragment B12 l'énonce ainsi : « Chaque chose une est et était, de la manière la plus manifeste, ce dont il y a le plus en elle » (ἀλλ' ὅτῳ πλεῖστα ἔνι, ταῦτα ἐνδηλότατα ἓν ἕκαστόν ἐστι καὶ ἦν)<ref>Anaxagore, fragment B12, fin, cité par Simplicius, ''Commentaire sur la Physique'', 156, 13-157, 4 (DK 59 B 12 ; LM 25 D27).</ref>.
Une chose tire donc son identité apparente des ingrédients qui y prédominent : un morceau d'or nous apparaît comme de l'or parce que l'or y est présent dans une proportion supérieure à celle des autres ingrédients<ref>Théophraste, dans Simplicius, ''Commentaire sur la Physique'', 27, 2-11 (DK 59 A 41) : chaque chose est caractérisée par ce qui prédomine en elle ; Aristote, ''Physique'', I, 4, 187b1-7.</ref>. Cette prédominance n'est jamais absolue, puisque tous les autres ingrédients demeurent présents, mais elle suffit à conférer à l'objet ses caractères perceptibles. Curd propose de la comprendre en termes de concentration ou de densité relative plutôt que de quantité brute<ref>Curd, 2007, p. 188-189.</ref>.
Le principe de prédominance permet de concilier l'ontologie du mélange universel avec l'expérience d'un monde d'objets distincts. Il rend compte aussi du changement : lorsque le pain devient chair, la chair déjà présente dans le pain vient s'ajouter à la chair du corps, tandis que les autres ingrédients se dispersent<ref>Aristote, ''Génération des animaux'', I, 18, 723a10-11 ; Simplicius, ''Commentaire sur la Physique'', 460, 4-20 (DK 59 A 45) ; Platon, ''Phédon'', 96c-d (DK 59 A 46).</ref>. Le changement apparent se ramène ainsi à une modification des proportions relatives des ingrédients.
Selon Théophraste, Anaxagore soutenait que la perception se fait par les contraires et que toute sensation s'accompagne de douleur, parce que le contact du dissemblable produit une gêne<ref>Théophraste, ''Du sens'', 27-29 (DK 59 A 92) ; Aétius, IV, 9, 16 (DK 59 A 94).</ref>. Les sens ne discernent que les différences marquées de proportion ; Anaxagore reconnaissait leur faiblesse, tout en affirmant que « les choses qui apparaissent sont une vue des choses invisibles » (ὄψις γὰρ τῶν ἀδήλων τὰ φαινόμενα)<ref>Anaxagore, fragments B21 (Sextus Empiricus, ''Contre les mathématiciens'', VII, 90 : « à cause de la faiblesse des sens, nous ne sommes pas capables de discerner le vrai ») et B21a (Sextus Empiricus, ''Contre les mathématiciens'', VII, 140) ; Schofield, 1980, p. 24-25.</ref>.
=== Synthèse : la réponse d'Anaxagore à Parménide ===
Les quatre principes examinés, à savoir la conservation, le « tout est dans tout », l'absence de plus petit et la prédominance, forment un ensemble cohérent qui constitue la réponse d'Anaxagore à l'exigence parménidienne<ref>Curd, 2007, p. 137-142 et 178-191 ; Schofield, 1980, p. 36-99.</ref>. Anaxagore accepte l'impossibilité du passage de l'être au non-être, mais rejette les conséquences que Parménide en tirait quant à l'unicité, à l'immobilité et à l'homogénéité de l'être.
En remplaçant la génération et la corruption par le mélange et la dissociation (B17), il rend compte des phénomènes sans enfreindre l'interdit éléate. En posant que tout est dans tout (B6, B11), il explique comment des substances en apparence nouvelles peuvent émerger sans naître de rien : elles étaient déjà présentes, mais imperceptibles. En affirmant qu'il n'y a pas de plus petit (B3, B6), il garantit que le mélange universel ne sera jamais défait. En introduisant le principe de prédominance (B12), il rend compte de la diversité des apparences et de la possibilité de la perception.
Anaxagore entend ainsi préserver la permanence de ce qui est et l'impossibilité du non-être tout en rendant compte de la pluralité et du devenir. Les spécialistes restent partagés sur le degré de son engagement éléate : Curd met l'accent sur la dimension parménidienne du système, Schofield sur sa fidélité au style dogmatique de la cosmologie ionienne<ref>Curd, 2007, p. 141-142 ; Schofield, 1980, p. 26-28 et 142-143 ; Daniel W. Graham, ''Explaining the Cosmos'', 2006.</ref>.
== Les ingrédients primordiaux ==
La question de savoir quels sont exactement les ingrédients (τὰ χρήματα) qui composent l'univers d'Anaxagore divise les commentateurs depuis Aristote<ref>Pour une vue d'ensemble : Schofield, 1980, p. 100-144 ; Curd, 2007, p. 147-150 et 153-191 ; Daniel W. Graham, « Was Anaxagoras a Reductionist? », ''Ancient Philosophy'', vol. 24, 2004, p. 1-18 ; Curd et Sisko, ''SEP'', 2026, § 3.2.</ref>. Anaxagore ne fournit pas de liste systématique, et les fragments conservés mentionnent des entités de natures apparemment différentes. Schofield observe que, malgré le « rôle cardinal » de la doctrine du « tout est dans tout », les fragments montrent qu'Anaxagore accordait au moins autant de place, dans son exposé, au mélange primordial et à l'action cosmogonique du Noûs<ref>Schofield, 1980, p. 100.</ref>.
=== Les opposés ===
Les fragments B4b, B8, B12 et B15 énumèrent plusieurs couples d'opposés : l'humide et le sec (τὸ διερόν καὶ τὸ ξηρόν), le chaud et le froid (τὸ θερμόν καὶ τὸ ψυχρόν), le brillant et l'obscur (τὸ λαμπρόν καὶ τὸ ζοφερόν), le dense et le rare (τὸ πυκνόν καὶ τὸ ἀραιόν)<ref>Anaxagore, B4b, B8, B12, B15. Les fragments B1 et B2 mentionnent l'air et l'éther, non des couples d'opposés.</ref>. Ces opposés jouent un rôle cosmologique central : c'est leur séparation progressive (ἀποκρίνεσθαι) à partir du mélange originel qui produit la diversité des phénomènes<ref>Anaxagore, B12, B13, B15, B16 ; Aristote, ''Physique'', I, 4, 187a25-26 et 187b1-7.</ref>.
Les opposés ne doivent pas être compris comme de simples attributs d'une matière sous-jacente. La distinction entre substance et qualité, et la notion même de « matière » (ὕλη), sont des élaborations aristotéliciennes qu'il est anachronique de projeter sur Anaxagore<ref>Curd, 2007, p. 141, n. 32. Burnet (1930, chap. VI) souligne que le chaud et le froid, le sec et l'humide sont pour Anaxagore des « choses » (χρήματα).</ref>. Le chaud n'est pas une propriété d'une matière indéterminée, mais une réalité présente en plus ou moins grande proportion dans un mélange. F. M. Cornford parle à leur propos de « choses-qualités » (''quality-things'')<ref>F. M. Cornford, « Anaxagoras' Theory of Matter », ''Classical Quarterly'', vol. 24, 1930, p. 14-30 et 83-95, ici p. 84 (cité par Teodorsson, 1982, p. 35).</ref> ; Gregory Vlastos les décrit comme des puissances dont la combinaison en certaines proportions produit les substances naturelles<ref>Vlastos, 1950, repris dans ''Studies in Greek Philosophy'', vol. I, éd. Daniel W. Graham, Princeton, Princeton University Press, 1995, p. 303-327, ici p. 322 (cité par Curd, 2007, p. 168, n. 31).</ref>.
=== L'interprétation « austère » : les opposés seuls ===
Paul Tannery fut l'un des premiers à contester l'interprétation aristotélicienne selon laquelle Anaxagore aurait posé des « homéomères » élémentaires (chair, os, etc.) et à faire des opposés les véritables constituants<ref>Paul Tannery, « La théorie de la matière d'Anaxagore », ''Revue philosophique'', vol. 22, 1886, p. 255-274 ; repris dans ''Pour l'histoire de la science hellène'', Paris, Alcan, 1887 (Burnet renvoie à la p. 283 sq.). Voir Teodorsson, 1982, p. 30.</ref>. John Burnet adopta une position voisine ; il remarquait que, même après la définition de la notion de qualité (ποιότης), cette manière de penser les opposés comme des choses avait survécu, et s'appuyait sur Galien, pour qui les qualités sont éternelles chez Anaxagore<ref>Burnet, 1930, chap. VI, § « The Portions », p. 263 et note, qui cite Galien, ''Des facultés naturelles'', I, 2.</ref>.
Cette interprétation présente plusieurs avantages. Elle s'appuie étroitement sur les fragments plutôt que sur les reconstructions d'Aristote, qui écrivait environ un siècle après Anaxagore avec ses propres préoccupations. Elle rend compte du rôle cosmologique des opposés dans les fragments B12, B15 et B16<ref>Schofield, 1980, p. 107-121 ; Curd, 2007, p. 156-157 et 163-166.</ref>. Elle a été défendue, sous des formes diverses, par Tannery, Burnet, Cornford, Vlastos, Schofield, Inwood, Sedley et Marmodoro<ref>Curd, 2007, p. 156, n. 8, et p. 164, n. 21 ; Marmodoro, 2017, chap. 1, § 1.1, n. 4 ; Curd et Sisko, ''SEP'', 2026, § 3.2 ; David Sedley, ''Creationism and Its Critics in Antiquity'', Berkeley, University of California Press, 2007. Schofield (1980, p. 114-116 et 132-133) distingue des ingrédients fondamentaux (les opposés) et des ingrédients dérivés (air, terre, eau, semences) ; il a ensuite nuancé sa position (Curd, 2007, p. 156, n. 8). Marmodoro comprend les opposés comme des puissances et tient les substances pour réductibles aux opposés.</ref>.
=== L'interprétation « expansive » et le témoignage d'Aristote ===
La question devient plus complexe lorsqu'on prend en compte les témoignages indirects. Aristote attribue à Anaxagore une doctrine des « homéomères » (τὰ ὁμοιομερῆ), substances dont les parties portent le même nom que le tout, comme la chair, l'os ou la moelle<ref>Aristote, ''Génération et corruption'', I, 1, 314a18-20 (DK 59 A 46) ; ''Du ciel'', III, 3, 302a28-b4 (DK 59 A 43) ; ''Physique'', I, 4, 187a25-26.</ref>.
Le terme d'homéomère n'apparaît pourtant dans aucun fragment. Il appartient au vocabulaire d'Aristote, pour qui les homéomères constituent un niveau intermédiaire entre les éléments et les organes<ref>Aristote, ''Parties des animaux'', II, 1, 646a12-24 ; Curd, 2007, p. 147-150. Burnet (1930, chap. VI) jugeait étrange, si Anaxagore avait employé le terme, que Simplicius ne cite aucun fragment qui le contienne.</ref>. Curd montre en outre qu'Aristote ne prête pas à Anaxagore un « principe d'homéomérie » au sens strict, et qu'il se trompe en faisant de l'air et du feu des mélanges d'homéomères<ref>Curd, 2007, p. 148-150.</ref>.
Une lecture « expansive » soutient néanmoins que tout ce qui apparaît dans le monde sensible se trouvait déjà dans le mélange originel. Elle a été défendue, sous des formes diverses, par Strang, Stokes, Guthrie, Barnes, Furth et Graham ; Peck en proposait une version qui écartait les substances inorganiques<ref>Curd, 2007, p. 154-156 et n. 3-4, p. 158-159, n. 12 ; Curd et Sisko, ''SEP'', 2026, § 3.2 ; Arthur L. Peck, « Anaxagoras: Predication as a Problem in Physics », ''Classical Quarterly'', vol. 25, 1931, p. 27-37 et 112-120. Kerferd (1969) admet une forme d'homéomérie compatible avec le mélange universel (Curd et Sisko, § 3.7).</ref>. Gershenson et Greenberg proposent de leur côté une lecture particulaire dans laquelle les tissus organiques, conçus comme des « molécules » infinitésimales, sont les éléments de toute matière<ref>Daniel E. Gershenson et Daniel A. Greenberg, ''Anaxagoras and the Birth of Scientific Method'', New York, Blaisdell, 1964, p. 9-12 et 14-22.</ref>.
=== L'interprétation modérée ===
Une troisième voie a été proposée par Patricia Curd, dont la position est proche de celle de W. Mann et a été reprise par John Sisko<ref>Curd, 2007, p. 157-171 ; W. Mann, « Anaxagoras and the ''Homoiomerē'' », ''Phronesis'', vol. 25, 1980, p. 228-249 ; Curd et Sisko, ''SEP'', 2026, § 3.2.</ref>. Selon cette lecture, les ingrédients comprennent les opposés et diverses substances : terre, air, éther, eau, feu, métaux, chair, sang, os. En revanche, les plantes, les animaux et leurs organes ne sont pas des ingrédients primordiaux, mais des composés temporaires, des « artefacts naturels » selon l'expression de Curd<ref>Curd, 2007, p. 157-163 et 170-171.</ref>.
Cette lecture tient compte à la fois des fragments et des témoignages d'Aristote. Elle reconnaît le rôle cosmologique des opposés (B12, B15) tout en admettant que d'autres substances figurent au même niveau dans le mélange : l'air et l'éther recouvrent tout avant même la rotation (B1), et B4b mentionne la terre et les semences à côté des opposés<ref>Curd, 2007, p. 166-167.</ref>.
=== Les semences (σπέρματα) ===
Les « semences » (σπέρματα), mentionnées dans les fragments B4a et B4b, ajoutent une difficulté supplémentaire. Selon B4b, avant toute séparation, aucune couleur n'était manifeste, car le mélange de toutes choses l'empêchait, « la terre étant présente en abondance et des semences illimitées en nombre, en rien semblables les unes aux autres » (καὶ γῆς πολλῆς ἐνεούσης καὶ σπερμάτων ἀπείρων πλῆθος οὐδὲν ἐοικότων ἀλλήλοις)<ref>Anaxagore, fragment B4b, cité par Simplicius, ''Commentaire sur la Physique'', 34, 21-26 (DK 59 B 4b). Sur la division de DK B4 en B4a et B4b, voir Curd, 2007, p. 42.</ref>. Le fragment B4a déclare : « Puisqu'il en est ainsi, il faut penser qu'il y a beaucoup de choses de toutes sortes dans toutes les choses qui se composent, et des semences de toutes choses, ayant des formes, des couleurs et des saveurs de toute espèce »<ref>Anaxagore, fragment B4a, cité par Simplicius, ''Commentaire sur la Physique'', 34, 29-35, 9 (DK 59 B 4a). Simplicius précise que le passage se trouvait peu après le début du livre.</ref>.
La nature de ces semences est débattue. Gregory Vlastos voyait dans σπέρμα un terme technique désignant un agrégat de tous les ingrédients dans lequel l'un d'eux prédomine ; G. E. R. Lloyd et W. K. C. Guthrie ont adopté des positions voisines<ref>Vlastos, 1950, repris dans Furley et Allen (éd.), 1975, vol. II, p. 323-353, ici p. 324 ; G. E. R. Lloyd, ''Polarity and Analogy'', Cambridge, Cambridge University Press, 1966, p. 246-247 ; Guthrie, 1965, p. 298-300. Voir Schofield, 1980, p. 123 et n. 38-39.</ref>. Une lecture aujourd'hui répandue, défendue par David Furley, Malcolm Schofield, Patricia Curd, David Sedley et Anna Marmodoro, comprend les semences au sens biologique ordinaire, comme les germes à partir desquels croissent plantes et animaux<ref>David J. Furley, « Anaxagoras in Response to Parmenides », ''Canadian Journal of Philosophy'', suppl. vol. 2, 1976, p. 61-85 ; Schofield, 1980, p. 123-126 ; Curd, 2007, p. 171-177 ; Sedley, 2007 ; Marmodoro, 2017, chap. 5.</ref>. D'autres en font des « homoncules » préformés (Edward Lewis), ou encore des « programmes » de développement (Teodorsson)<ref>Edward Lewis, « Anaxagoras and the Seeds of a Physical Theory », ''Apeiron'', vol. 33, 2000, p. 1-23 ; Teodorsson, 1982, p. 85 et 89-91, qui recense aussi les interprétations antérieures (p. 45-64) ; critique de la lecture en homoncules chez Curd, 2007, p. 172-175.</ref>. Aristote, pour sa part, semble avoir rangé sous ce terme les homéomères eux-mêmes<ref>Aristote, ''Du ciel'', III, 3, 302a28-b4 (DK 59 A 43) ; ''Génération et corruption'', I, 1, 314a28-b1 ; Curd, 2007, p. 151-152 ; Schofield, 1980, p. 128-132.</ref>.
La lecture biologique ne fait pas violence au langage : le mot est employé dans son sens ordinaire, dans un contexte (B4a) où il est question d'hommes, d'animaux et de cultures. Elle s'accorde avec d'autres témoignages. Théophraste rapporte qu'Anaxagore disait l'air porteur de semences de toutes choses, qui, entraînées avec l'eau de pluie, engendrent les plantes<ref>Théophraste, ''Recherches sur les plantes'', III, 1, 4 (DK 59 A 117 ; cf. ''Causes des plantes'', I, 5, 2) ; Irénée, ''Contre les hérésies'', II, 14, 2 (DK 59 A 113) ; Schofield, 1980, p. 124-126.</ref>. Diogène Laërce rapporte que les animaux naquirent d'abord de l'humide, du chaud et du terreux, puis les uns des autres<ref>Diogène Laërce, II, 9 (DK 59 A 1) ; cf. Hippolyte, ''Réfutation de toutes les hérésies'', I, 8, 12 (DK 59 A 42) : les animaux naquirent d'abord dans l'humide, puis les uns des autres.</ref>.
== L'état originel : tout ensemble ==
Le traité d'Anaxagore s'ouvrait sur l'une des déclarations les plus célèbres de la philosophie présocratique : « Toutes choses étaient ensemble » (ὁμοῦ χρήματα πάντα ἦν)<ref>Anaxagore, fragment B1, cité par Simplicius, ''Commentaire sur la Physique'', 155, 26-30 (DK 59 B 1 ; LM 25 D9). L'ordre ὁμοῦ πάντα χρήματα ἦν, souvent cité, provient de la tradition indirecte ; sur le texte original, voir Wolfgang Rösler, « ΟΜΟΥ ΧΡΗΜΑΤΑ ΠΑΝΤΑ ΗΝ », ''Hermes'', vol. 99, 1971, p. 246-248 (Schofield, 1980, p. 151, n. 1). La phrase citée par Diogène Laërce (II, 6), « Toutes choses étaient ensemble ; puis l'Intellect vint et les ordonna », est un résumé et non un fragment (Burnet, 1930, chap. VI).</ref>. Cette formule d'ouverture exprime la thèse cosmogonique d'Anaxagore sur l'état primordial de l'univers ; Schofield souligne que le livre commençait sans préambule personnel, en exposant d'emblée ce thème<ref>Schofield, 1980, p. 36-40.</ref>.
La formulation d'Anaxagore répond à Parménide<ref>Owen, 1966, repris dans Mourelatos (éd.), 1974, p. 276-277 ; Schofield, 1980, p. 64.</ref>. Là où Parménide affirmait que l'être « est maintenant tout entier ensemble, un, continu » (νῦν ἔστιν ὁμοῦ πᾶν, ἕν, συνεχές)<ref>Parménide, B 8, 5-6 (DK 28 B 8).</ref>, Anaxagore proclame que « toutes choses étaient ensemble ». À l'unité, à la continuité et au présent intemporel de l'être parménidien, il oppose la pluralité, la divisibilité illimitée, un état passé et un devenir cosmogonique<ref>Schofield, 1980, p. 64-65 ; Curd, 2007, p. 153-154.</ref>.
=== La description du mélange originel ===
Le fragment B1, que Simplicius dit placé au début du premier livre, décrit cet état primordial :
<blockquote>Toutes choses étaient ensemble, illimitées en quantité et en petitesse, car le petit aussi était illimité. Et toutes choses étant ensemble, rien n'était manifeste en raison de la petitesse ; car l'air et l'éther recouvraient toutes choses, étant l'un et l'autre illimités : ce sont eux, en effet, les plus grands dans l'ensemble des choses, et en quantité et en grandeur.<ref>Anaxagore, fragment B1 (DK 59 B 1).</ref></blockquote>
Quatre caractéristiques du mélange originel y sont énoncées : toutes choses étaient ensemble ; elles étaient illimitées en quantité et en petitesse ; rien n'était manifeste en raison de la petitesse ; l'air et l'éther recouvraient toutes choses.
L'expression « illimitées en quantité et en petitesse » a donné lieu à deux lectures principales, que Schofield a nommées interprétation « particulaire » et interprétation « proportionnelle »<ref>Schofield, 1980, p. 70-79.</ref>. La première comprend que le mélange originel contenait une infinité de petites particules distinctes<ref>Vlastos, 1950 ; Guthrie, 1965, p. 289 ; Kerferd, 1969 ; Sider, 1981 (voir Curd, 2007, p. 183, n. 9 ; Curd et Sisko, ''SEP'', 2026, § 3.3).</ref>. La seconde refuse de concevoir le mélange comme une collection de particules : chaque ingrédient y est présent en une proportion aussi faible qu'on voudra par rapport à l'ensemble<ref>Schofield, 1980, p. 73-79 ; Barnes, 1982, p. 323-326 ; Inwood, 1986, p. 19-22. Curd (2007, p. 181-187) propose un modèle de « densités » : les ingrédients, comparables à des liquides ou à des pâtes, sont présents partout à des concentrations variables.</ref>. Le débat n'est pas tranché<ref>Pour une discussion d'ensemble, voir Curd, 2007, p. 181-191, et Marmodoro, 2017, chap. 4 (p. 105-127).</ref>.
=== L'imperceptibilité du mélange ===
La troisième caractéristique du mélange originel est son indistinction : « rien n'était manifeste » (οὐδὲν ἔνδηλον ἦν). Anaxagore l'explique par deux facteurs : la petitesse (ὑπὸ σμικρότητος) et le fait que « l'air et l'éther recouvraient toutes choses » (πάντα γὰρ ἀήρ τε καὶ αἰθὴρ κατεῖχεν).
L'air (ἀήρ) désigne ici, selon l'usage ionien ancien, une brume sombre, humide, froide et dense ; l'éther (αἰθήρ), la substance brillante, chaude, sèche et rare, qu'Anaxagore identifie au feu<ref>Théophraste, ''Du sens'', 59 (DK 59 A 70) : le rare et le fin sont chauds, le dense et l'épais froids, comme Anaxagore définit l'éther et l'air ; Aristote, ''Du ciel'', I, 3, 270b24-25 (DK 59 A 73), et III, 3, 302b4 ; Schofield, 1980, p. 71.</ref>. Ces deux substances, dit B1, sont les plus grandes dans l'ensemble des choses, en quantité et en grandeur : elles prédominaient dans le mélange originel, à la manière d'un brouillard qui recouvre tout<ref>Curd, 2007, p. 178 et n. 1 ; Schofield, 1980, p. 155-156, n. 5-6.</ref>.
Le fragment B4b confirme cette description :
<blockquote>Mais avant que ces choses ne fussent séparées, toutes étant ensemble, aucune couleur n'était manifeste ; car le mélange de toutes choses l'empêchait, celui de l'humide et du sec, du chaud et du froid, du brillant et de l'obscur, la terre étant présente en abondance et des semences illimitées en nombre, en rien semblables les unes aux autres.<ref>Anaxagore, fragment B4b (DK 59 B 4b).</ref></blockquote>
L'indistinction du mélange originel ne tenait donc pas à l'absence des ingrédients, mais à leur mélange si intime qu'aucun ne pouvait se manifester. Curd souligne qu'il s'agit d'un contrefactuel : aucun observateur n'était présent, mais un observateur n'aurait rien pu y distinguer<ref>Curd, 2007, p. 46.</ref>.
=== L'immobilité originelle ===
Plusieurs témoignages rapportent qu'avant l'intervention du Noûs, le mélange était au repos depuis un temps illimité<ref>Aristote, ''Physique'', VIII, 1, 250b24-26 ; Simplicius, ''Commentaire sur la Physique'', 1121, 21 (DK 59 A 64) ; Aétius, I, 7, 5 (DK 59 A 48).</ref>. Cette immobilité pose une difficulté : si le mélange était au repos, qu'est-ce qui a pu le mettre en mouvement ? Anaxagore répond en posant le Noûs (Νοῦς, Intellect), distinct de tous les ingrédients, qui possède le pouvoir d'initier le mouvement<ref>Anaxagore, B12 et B13.</ref>. Eudème reprochait déjà à Anaxagore de faire commencer le mouvement à un moment donné sans dire s'il cesserait un jour<ref>Simplicius, ''Commentaire sur la Physique'', 1185, 9 (DK 59 A 59). Une colonne d'un papyrus d'Herculanum (Philodème) attribue au contraire à Anaxagore l'idée d'un mouvement éternel ; voir Christian Vassallo, ''The Presocratics at Herculaneum'', Berlin et Boston, De Gruyter, 2021, et Curd et Sisko, ''SEP'', 2026, § 4.4.</ref>.
=== L'étendue du mélange originel ===
Le fragment B1 affirme que l'air et l'éther « étaient l'un et l'autre illimités » (ἀμφότερα ἄπειρα ἐόντα), et le fragment B2 précise que « l'air et l'éther se séparent de la masse environnante, et la masse environnante est illimitée en quantité »<ref>Anaxagore, fragment B2 (DK 59 B 2).</ref>. Le mélange originel était donc spatialement illimité<ref>Aristote, ''Physique'', III, 4, 203a19-33 (DK 59 A 45) ; III, 5, 205b1-5 (DK 59 A 50), où Aristote critique l'idée que l'illimité se fixe lui-même en place.</ref>. Le fragment B12 indique que la révolution a commencé à partir d'une petite région, qu'elle s'étend maintenant davantage et qu'elle s'étendra davantage encore<ref>Anaxagore, B12 (DK 59 B 12).</ref>. Le processus cosmogonique n'a donc pas encore affecté la totalité du mélange : la description de B1 vaut toujours pour les régions que la rotation n'a pas atteintes<ref>Curd, 2007, p. 207-208.</ref>. Cette conception d'un univers partiellement ordonné, dont la formation se poursuit à la périphérie, compte parmi les idées les plus originales d'Anaxagore.
== Le Noûs : l'Intellect cosmique ==
Le fragment B12, le plus long des fragments conservés, est presque entièrement consacré au Noûs (Νοῦς, « Intellect » ou « Esprit »). Schofield y voit l'un des passages les plus puissants de toute la prose grecque, par son intensité et sa lente grandeur<ref>Schofield, 1980, p. 4 ; sur le style de « prédication solennelle », voir Deichgräber, 1933, et Schofield, 1980, p. 6-9.</ref>. Anaxagore y expose la nature du Noûs et son rôle dans la formation du monde. La tradition a souvent tenu cette doctrine pour son apport le plus original ; Diogène Laërce et Plutarque rapportent qu'on le surnommait lui-même « Noûs », surnom qui pouvait être moqueur autant qu'admiratif<ref>Diogène Laërce, II, 6 (DK 59 A 1) ; Plutarque, ''Vie de Périclès'', 4 (DK 59 A 15) ; Curd, 2007, p. 192. Burnet (1930, chap. VI) jugeait au contraire que l'originalité d'Anaxagore tenait davantage à sa théorie de la substance qu'à celle du Noûs.</ref>.
Deux points doivent être distingués. Le Noûs est d'abord la source du mouvement : il initie et contrôle la révolution qui produit la séparation des ingrédients à partir du mélange originel. Il est aussi un intellect : il connaît, discerne et ordonne. En revanche, aucun fragment conservé n'affirme que l'Intellect dispose les choses de la manière qui est la meilleure. C'est précisément ce que Socrate, dans le ''Phédon'', aurait voulu trouver chez Anaxagore, et dont il déplore l'absence<ref>Platon, ''Phédon'', 97b-98c (DK 59 A 47) ; Curd, 2007, p. 144 et 192.</ref>. L'identification du Noûs à une cause finale organisant le monde en vue du bien doit donc beaucoup à la lecture de Platon, puis d'Aristote.
Appliquer aux présocratiques la distinction aristotélicienne des quatre causes (matérielle, formelle, efficiente, finale) est par ailleurs anachronique : cette classification est une construction d'Aristote, élaborée pour situer ses prédécesseurs par rapport à sa propre doctrine<ref>Patricia Curd, « Presocratic Philosophy », ''Stanford Encyclopedia of Philosophy'', 2007, révision substantielle 2020, § 2 ; Curd, 2007, p. 141, n. 32, et p. 198, où elle qualifie elle-même d'anachronique l'expression de « cause efficiente ».</ref>. Le Noûs ne se laisse ranger entièrement dans aucune de ces catégories. La question de savoir s'il poursuit des fins, et en quel sens, fait l'objet d'un débat qui est présenté plus loin.
=== La séparation du Noûs d'avec toutes choses ===
Le fragment B12 s'ouvre par une affirmation qui constitue la thèse centrale d'Anaxagore concernant le Noûs :
<blockquote>Les autres choses ont part à toute chose, mais l'Intellect est illimité et maître de lui-même, il n'est mêlé à aucune chose, mais il est seul, lui-même par lui-même.<ref>Anaxagore, fragment B12, cité par Simplicius, ''Commentaire sur la Physique'', 156, 13-15 (DK 59 B 12). Simplicius cite la première proposition en 164, 24-25.</ref></blockquote>
Le Noûs fait ainsi exception au principe du mélange universel : alors que tous les ingrédients contiennent des parts de tous les autres, lui seul demeure pur et séparé<ref>Anaxagore, B11 (DK 59 B 11).</ref>. Trois attributs lui sont d'abord prédiqués : il est « illimité » (ἄπειρον), « maître de lui-même » (αὐτοκρατές) et « mêlé à aucune chose ». Anaxagore justifie cette séparation par un argument :
<blockquote>Car s'il n'était pas par lui-même, mais s'il était mêlé à quelque autre chose, il aurait part à toutes choses, s'il était mêlé à l'une d'elles ; car en toute chose il y a une part de toute chose, comme je l'ai dit auparavant. Et les choses mêlées à lui l'empêcheraient, de sorte qu'il ne dominerait aucune chose comme il le fait, étant seul par lui-même.<ref>Anaxagore, B12, cité par Simplicius, ''Commentaire sur la Physique'', 156, 15-20.</ref></blockquote>
Il s'agit d'une preuve indirecte : si le Noûs était mêlé à quoi que ce soit, il serait mêlé à tout ; les choses mêlées à lui l'empêcheraient d'exercer son pouvoir ; or il l'exerce ; il n'est donc mêlé à rien<ref>Schofield, 1980, p. 7 et 19, qui voit dans l'usage de cette forme d'argument un possible indice d'influence éléate (p. 146, n. 15).</ref>. Anaxagore n'explique pas pourquoi le mélange entraverait l'action du Noûs, et plusieurs explications ont été proposées<ref>Curd, 2007, p. 58-59 et 200-201 ; Schofield, 1980, p. 147-148, n. 39.</ref>.
=== Les attributs du Noûs ===
Après avoir établi la séparation du Noûs, Anaxagore énonce une série d'attributs dans le style de la prédication solennelle, qu'Eduard Norden et Karl Deichgräber ont rapproché de l'hymne religieux<ref>Deichgräber, 1933, p. 347-361 ; Eduard Norden, ''Agnostos Theos'', Leipzig, Teubner, 1913 (4{{e}} tirage, Stuttgart, 1956), p. 143-176 ; Schofield, 1980, p. 6-9 et 12.</ref> :
<blockquote>Car il est la plus fine de toutes les choses et la plus pure ; il détient tout discernement (γνώμη) sur toute chose et il a la plus grande force ; et toutes les choses qui ont une âme, les plus grandes comme les plus petites, l'Intellect les domine.<ref>Anaxagore, B12, cité par Simplicius, ''Commentaire sur la Physique'', 156, 20-24.</ref></blockquote>
Le premier attribut, qui qualifie le Noûs de « plus fin » (λεπτότατον) et « plus pur » (καθαρώτατον), a reçu des interprétations divergentes, que Schofield ramène à trois<ref>Schofield, 1980, p. 11-12.</ref>. Pour les uns, il s'agit d'une substance matérielle d'une extrême finesse : Anaxagore emploie probablement λεπτός en un sens physique à propos de l'eau de mer, et il parle d'un intellect « plus grand » ou « plus petit »<ref>Aétius, III, 16, 2 (DK 59 A 90) ; Burnet, 1930, p. 268 ; J. E. Raven, « The Basis of Anaxagoras' Cosmology », ''Classical Quarterly'', n.s. vol. 4, 1954, p. 123-137, ici p. 134-135 ; Barnes, 1982, p. 406-409 ; Sider, 1981. Voir Schofield, 1980, p. 147, n. 28, et Curd, 2007, p. 59, n. 50.</ref>. Pour d'autres, ces termes visent à exprimer l'incorporéité du Noûs<ref>Guthrie, 1965, p. 276-278 ; Curd, 2007, p. 59 et 200 ; Gershenson et Greenberg, 1964, p. 32-33, qui font d'Anaxagore le premier penseur grec à introduire une entité substantielle incorporelle.</ref>. Une troisième lecture, celle d'Aristote, à laquelle Schofield se rallie, comprend que le Noûs n'a aucun caractère propre qui le rendrait semblable aux choses qu'il connaît et domine<ref>Aristote, ''De l'âme'', III, 4, 429a18-24 (DK 59 A 100) ; Schofield, 1980, p. 11-12.</ref>. On a longtemps soutenu que la notion d'incorporéité n'était pas encore disponible à l'époque d'Anaxagore ; Zeller jugeait qu'il avait voulu parler d'un incorporel sans y parvenir<ref>Burnet, 1930, chap. VI, qui rapporte la position de Zeller.</ref>. Curd rappelle toutefois que Mélissos refuse déjà un corps à l'Un<ref>Mélissos, DK 30 B 9 ; Curd, 2007, p. 59.</ref>.
Le deuxième attribut concerne la connaissance : le Noûs « détient tout discernement sur toute chose ». Le troisième concerne la puissance : il « a la plus grande force ». Anaxagore suggère le lien entre les deux par une assonance, ἴσχει (« il détient ») et ἰσχύει (« il a de la force »)<ref>Schofield, 1980, p. 15 et p. 147, n. 36 ; Curd, 2007, p. 60. James Lesher comprend γνώμη au sens de décision : le Noûs décide de tout ce qui advient (« Mind's Knowledge and Powers of Control in Anaxagoras DK B12 », ''Phronesis'', vol. 40, 1995, p. 125-142).</ref>. Le quatrième attribut, le contrôle exercé sur tous les êtres animés, s'accorde avec B11, selon lequel l'Intellect est présent dans certaines choses. Schofield montre que tout le passage peut se lire aussi bien comme une description d'un Intellect suprême que comme une thèse sur l'intellect en général, et que cette ambiguïté est probablement inhérente au texte<ref>Schofield, 1980, p. 10-22 ; Curd, 2007, p. 60-61.</ref>.
=== Le rôle cosmogonique du Noûs ===
Après avoir décrit la nature du Noûs, Anaxagore expose son rôle dans la cosmogonie :
<blockquote>Et l'Intellect a dominé la révolution entière, de sorte qu'elle a commencé à tourner au commencement. Elle a d'abord commencé à tourner à partir d'une petite région, mais elle tourne sur une région plus grande, et elle tournera sur une région plus grande encore. Et les choses qui se mêlent, qui se séparent et qui se dissocient, l'Intellect les a toutes connues. Et celles qui devaient être, celles qui étaient et ne sont plus, celles qui sont maintenant et celles qui seront, l'Intellect les a toutes ordonnées, ainsi que cette révolution dans laquelle tournent maintenant les astres, le soleil, la lune, l'air et l'éther qui se séparent.<ref>Anaxagore, B12, cité par Simplicius, ''Commentaire sur la Physique'', 156, 24-157, 2.</ref></blockquote>
Ce passage affirme trois choses. L'Intellect initie une révolution (περιχώρησις) dans un mélange jusqu'alors immobile. Ce mouvement, commencé dans une petite région, s'étend progressivement et continue de s'étendre : la cosmogonie n'est pas un événement révolu, mais un processus qui se poursuit à la périphérie du mélange illimité. Enfin, c'est ce mouvement qui produit la séparation (ἀπόκρισις) et la dissociation (διάκρισις) des ingrédients, d'où naît le cosmos ordonné que nous observons<ref>Anaxagore, B12 et B13 ; Simplicius, ''Commentaire sur la Physique'', 300, 27-301, 1 ; Aristote, ''Physique'', VIII, 1, 250b24-26.</ref>.
Le mécanisme de la séparation est d'ordre physique : la rotation, par sa vitesse et sa force, rassemble au centre les ingrédients denses, humides, froids et obscurs, et repousse vers la périphérie les ingrédients rares, chauds, secs et brillants<ref>Anaxagore, B9, B12, B15 ; Aristote, ''Du ciel'', II, 13, 295a9-14 (DK 59 A 88), qui rapporte que, dans les tourbillons, les corps les plus lourds se portent vers le centre ; Curd, 2007, p. 207-208.</ref>. Le Noûs n'intervient donc pas dans chaque détail : il initie et gouverne la rotation, qui produit ensuite la séparation et la recomposition des ingrédients. C'est pourquoi Platon et Aristote pourront reprocher à Anaxagore de ne plus recourir à l'Intellect dans l'explication des phénomènes particuliers. Simplicius répondait déjà que le Noûs demeure la cause première, puisque la génération est séparation, que la séparation résulte du mouvement et que le Noûs est cause du mouvement<ref>Simplicius, ''Commentaire sur la Physique'', 300, 27-31 ; Curd, 2007, p. 202-203.</ref>.
Le texte affirme aussi que « l'Intellect a ordonné » (διεκόσμησε νοῦς) toutes choses, passées, présentes et futures. Le verbe διακοσμεῖν signifie « disposer », « mettre en ordre ». Le Noûs connaît et dispose ; il est à la fois intelligent et ordonnateur. Mais aucun fragment n'explique cet ordre par le meilleur, et la portée téléologique de la doctrine reste débattue. De nombreux interprètes contemporains attribuent au Noûs une forme de téléologie : une téléologie « mince », qui lie sa connaissance à son action (Lesher, Curd, Pinto), ou « épaisse », qui lui prête des buts déterminés, comme la connaissance par discernement des ingrédients (André Laks) ou la production du monde le plus favorable à la vie humaine (David Sedley)<ref>Curd et Sisko, ''SEP'', 2026, § 4.2 ; Lesher, 1995 ; Curd, 2007, p. 194 et 204-205 ; Rhodes Pinto, « ''Nous'', Motion, and Teleology in Anaxagoras », ''Oxford Studies in Ancient Philosophy'', vol. 52, 2017, p. 1-32 ; André Laks, « Mind's Crisis: On Anaxagoras' ''Nous'' », ''Southern Journal of Philosophy'', vol. 31, suppl., 1993, p. 19-38 ; Sedley, 2007. Anna Marmodoro voit en Anaxagore le premier à proposer une approche téléologique de la cosmologie, le Noûs développant le monde selon sa conception de l'ordre à partir de ce qui est donné (Marmodoro, 2017, p. 129-130).</ref>. D'autres comprennent le Noûs comme une loi de la nature plutôt que comme un agent qui poursuit des fins<ref>Sider, 1981, selon Curd et Sisko, ''SEP'', 2026, § 4.1-4.2 ; Gershenson et Greenberg, 1964, p. 25 ; John E. Sisko, « Anaxagoras betwixt Parmenides and Plato », ''Philosophy Compass'', vol. 5, 2010, p. 432-442, qui estime qu'aucun argument probant n'a été opposé à la lecture de Platon.</ref>. Curd, pour sa part, nie que le Noûs soit un principe téléologique extérieur fixant un plan au cosmos, tout en laissant ouverte la possibilité d'un ordre immanent aux processus qu'il déclenche<ref>Curd, 2007, p. 144 et 204-205.</ref>.
=== La critique platonicienne et aristotélicienne ===
Dans le ''Phédon'', Platon fait raconter par Socrate sa déception à la lecture du livre d'Anaxagore :
<blockquote>Un jour, j'entendis quelqu'un lire dans un livre d'Anaxagore, disait-il, que c'est l'Intellect qui met tout en ordre et qui est la cause de toutes choses. Cette cause me réjouit, et il me sembla qu'il était bon, d'une certaine manière, que l'Intellect fût la cause de tout ; et je pensai que, s'il en est ainsi, l'Intellect, en ordonnant, ordonne tout et dispose chaque chose de la manière qui est la meilleure. [...] Mais cette merveilleuse espérance, mon ami, me fut ôtée lorsque, poursuivant ma lecture, je vis un homme qui ne faisait aucun usage de l'Intellect, qui ne lui attribuait aucune responsabilité dans l'ordonnance des choses, mais qui alléguait comme causes des airs, des éthers, des eaux et bien d'autres choses étranges.<ref>Platon, ''Phédon'', 97b-98c (DK 59 A 47).</ref></blockquote>
La portée de ce passage doit être saisie exactement. Socrate ne dit pas qu'Anaxagore avait proposé une explication par le meilleur et l'avait mal développée : il dit qu'il ''espérait'' en trouver une, et qu'il fut déçu. La formule « disposer chaque chose de la manière qui est la meilleure » exprime l'attente socratique, non la doctrine d'Anaxagore.
Le texte d'Anaxagore, dans les fragments que nous possédons, affirme que le Noûs connaît toutes choses et qu'il les ordonne par l'intermédiaire de la révolution cosmique. Il n'affirme pas que cet ordre soit le meilleur possible. C'est Socrate qui, lisant Anaxagore, infère que, si le monde est ordonné par un Intellect, il doit l'être en vue du bien. Curd juge que Platon a raison de constater l'absence, chez Anaxagore, d'un bien indépendant qui servirait de principe d'explication ; elle note que cette exigence, chez Platon, conduira à la Forme du Bien, puis au Démiurge du ''Timée''<ref>Curd, 2007, p. 144-145 et 204.</ref>. Il faut donc distinguer l'attribution au Noûs d'un rôle moteur et cognitif, qui est anaxagoréenne, et celle d'un principe évaluatif selon lequel le monde serait disposé en vue du meilleur, qui ne se trouve pas dans les fragments. Le reproche socratique ne dénonce pas une incohérence interne ; il regrette qu'Anaxagore ne soit pas allé jusqu'à l'explication que Socrate attendait.
Aristote reprend, dans la ''Métaphysique'', le reproche d'un usage insuffisant du Noûs :
<blockquote>Anaxagore se sert de l'Intellect comme d'un ''deus ex machina'' pour la fabrication du monde ; et quand il est embarrassé pour dire par quelle cause une chose est nécessairement, il le fait intervenir ; mais dans les autres cas, il donne pour causes de ce qui arrive toutes choses plutôt que l'Intellect.<ref>Aristote, ''Métaphysique'', A, 4, 985a18-21 (DK 59 A 47).</ref></blockquote>
Aristote précise ailleurs que ceux qui posent l'Intellect ou l'Amitié comme causes les traitent comme des principes du mouvement plutôt que comme des fins<ref>Aristote, ''Métaphysique'', A, 7, 988b6-16.</ref>. Ses autres objections, comme le souligne Curd, ne portent pas d'abord sur l'absence de téléologie : elles visent la confusion entre l'âme et l'intellect, et l'absence d'explication de la manière dont le Noûs connaît<ref>Aristote, ''De l'âme'', I, 2, 404a25-b6 et 405a13-19 (DK 59 A 55, A 99, A 100) ; III, 4, 429a18-24 et 429b22-24 (DK 59 A 100) ; Curd, 2007, p. 146 et 205.</ref>. L'explication par le meilleur, au sens où Socrate l'attendait, ne se trouve donc pas dans les fragments d'Anaxagore ; elle se constitue dans la critique platonicienne, puis dans la doctrine aristotélicienne de la cause finale. Certains interprètes modernes n'en reconnaissent pas moins, chez Anaxagore, une première forme de téléologie cosmique.
=== Synthèse : l'innovation du Noûs ===
L'introduction du Noûs constitue une innovation à plusieurs titres.
Anaxagore est, parmi les penseurs grecs dont nous connaissons les doctrines, le premier à poser explicitement une entité qui, bien qu'elle agisse sur les ingrédients, n'est mêlée à aucun d'eux<ref>Guthrie, 1965, p. 276-279 ; Curd, 2007, p. 193-194 et 200-201. Aristote (''Métaphysique'', A, 3, 984b15-20 = DK 59 A 58) signale que la doctrine était attribuée avant lui à Hermotime de Clazomènes, figure largement légendaire (Curd, 2007, p. 205, n. 23).</ref>. Il s'inscrit néanmoins dans une tradition : l'idée d'une intelligence cosmique qui comprend et gouverne l'univers se trouve déjà chez Xénophane et Héraclite<ref>Curd, 2007, p. 195 et n. 6-7.</ref>. Sa conception préparera, sans leur être équivalente, les doctrines ultérieures de l'âme séparée chez Platon et de l'intellect séparé chez Aristote<ref>Platon, ''Phédon'', 78b-84b ; Aristote, ''De l'âme'', III, 4-5.</ref>.
Anaxagore identifie par ailleurs une cause unique du mouvement cosmique et de l'ordre qui en résulte. Aristote salue cette innovation en disant qu'Anaxagore parut « comme un homme sobre » au milieu de prédécesseurs qui parlaient au hasard<ref>Aristote, ''Métaphysique'', A, 3, 984b15-20 (DK 59 A 58).</ref>. En nommant ce principe Νοῦς, Anaxagore suggère que l'ordre du monde n'est pas aveugle. Socrate, Platon et Aristote reprendront cette suggestion en la transformant profondément, au-delà de ce qu'affirment les fragments<ref>Schofield, 1980, p. 59-61, qui juge le Noûs plus proche du Démiurge platonicien que du Dieu créateur de la tradition judéo-chrétienne ; Curd, 2007, p. 143-146.</ref>.
== La cosmogonie et la cosmologie ==
La cosmogonie et la cosmologie d'Anaxagore appliquent ses principes métaphysiques. Anaxagore s'y montre héritier de la tradition ionienne et novateur, proposant des explications naturalistes des phénomènes célestes et météorologiques<ref>Curd, 2007, p. 206-234 ; Gershenson et Greenberg, 1964, p. 34-54 ; Daniel W. Graham, ''Science before Socrates: Parmenides, Anaxagoras, and the New Astronomy'', New York, Oxford University Press, 2013.</ref>. Les témoignages sont ici notre source principale, et ils ne concordent pas toujours.
=== Le mouvement rotatoire cosmogonique ===
Le processus cosmogonique commence lorsque le Noûs imprime au mélange originel un mouvement de révolution (περιχώρησις)<ref>Anaxagore, B12 et B13 (DK 59 B 12-13).</ref>. Ce mouvement prend la forme d'un tourbillon (δίνη) dont l'étendue ne cesse de croître<ref>Aristote, ''Du ciel'', II, 13, 295a9-14 (DK 59 A 88) ; Curd, 2007, p. 207-208.</ref>. Anaxagore affirme que sa rapidité est sans commune mesure avec celle d'aucune chose connue des hommes : elle est « de beaucoup de fois plus rapide »<ref>Anaxagore, fragment B9 (DK 59 B 9).</ref>.
La séparation qui en résulte est d'ordre physique : le tourbillon rassemble au centre les ingrédients denses et lourds et repousse vers la périphérie les ingrédients rares et légers, comme on l'observe dans les tourbillons d'eau ou d'air<ref>Aristote, ''Du ciel'', II, 13, 295a9-14 (DK 59 A 88) ; Hippolyte, ''Réfutation'', I, 8, 2 (DK 59 A 42).</ref>. Le fragment B15 décrit ce processus :
<blockquote>Le dense, l'humide, le froid et l'obscur se rassemblèrent ici, là où se trouve maintenant la terre ; le rare, le chaud et le sec se retirèrent vers les régions lointaines de l'éther.<ref>Anaxagore, fragment B15, cité par Simplicius, ''Commentaire sur la Physique'', 179, 3-6 (DK 59 B 15). Hippolyte ajoute « le brillant » à la seconde série.</ref></blockquote>
Cette séparation n'est jamais achevée : conformément au principe selon lequel rien n'est complètement séparé (B8, B12), la rotation continue de produire des séparations et des mélanges partiels<ref>Anaxagore, B8 (« les choses qui sont dans l'unique cosmos ne sont pas séparées les unes des autres ni tranchées à la hache ») et B12.</ref>.
=== La formation de la terre ===
Au centre du tourbillon, les ingrédients denses, humides, froids et obscurs se sont concentrés pour former la terre<ref>Anaxagore, B15 et B16 ; Hippolyte, ''Réfutation'', I, 8, 2 (DK 59 A 42).</ref>. Selon Anaxagore, la terre est plate<ref>Hippolyte, ''Réfutation'', I, 8, 3 (DK 59 A 42) ; Aristote, ''Du ciel'', II, 13, 294b13-21 (DK 59 A 87).</ref>, conception traditionnelle dans la cosmologie ionienne.
Elle demeure immobile parce qu'elle repose sur l'air qui la supporte. Selon Aristote, Anaximène, Anaxagore et Démocrite expliquaient cette stabilité par la platitude de la terre : elle ne fend pas l'air situé au-dessous, mais le recouvre comme un couvercle<ref>Aristote, ''Du ciel'', II, 13, 294b13-21 (DK 59 A 87). Hippolyte (I, 8, 3 = DK 59 A 42) invoque aussi la grandeur de la terre et l'absence de vide.</ref>. Aristote rapporte qu'Anaxagore montrait la résistance de l'air au moyen d'outres gonflées et de clepsydres<ref>Aristote, ''Physique'', IV, 6, 213a22-27 (DK 59 A 68) ; pseudo-Aristote, ''Problèmes'', XVI, 8, 914b9-915a24 (DK 59 A 69) ; Gershenson et Greenberg, 1964, p. 40-43. La clepsydre désigne ici un ustensile servant à transvaser les liquides (Curd, 2007, p. 108, n. 28).</ref>.
=== La formation des corps célestes ===
Les corps célestes se seraient formés à partir de pierres arrachées à la terre par la violence de la rotation, puis enflammées par l'éther brûlant qui les emporte<ref>Aétius, II, 13, 3 (DK 59 A 71) ; Hippolyte, ''Réfutation'', I, 8, 6 (DK 59 A 42) ; Curd, 2007, p. 209.</ref>. Les astres ne sont donc pas des êtres divins, mais des pierres incandescentes<ref>Platon, ''Apologie'', 26d (DK 59 A 35) ; Hippolyte, ''Réfutation'', I, 8, 6 (DK 59 A 42). Cette thèse est au cœur des accusations d'impiété.</ref>.
Anaxagore soutenait que le soleil est une masse de métal incandescent ou une pierre de feu, plus grande que le Péloponnèse<ref>Diogène Laërce, II, 8 (DK 59 A 1) ; Hippolyte, ''Réfutation'', I, 8, 8 (DK 59 A 42) ; Aétius, II, 20, 6 et II, 21, 3 (DK 59 A 72). Sur la taille du soleil, voir David Sider, « Anaxagoras on the Size of the Sun », ''Classical Philology'', vol. 68, 1973, p. 128-129.</ref>. L'estimation paraissait extravagante à une époque où l'on voyait dans le soleil un disque de dimensions modestes ; elle reste très inférieure à la taille réelle de l'astre. Daniel Graham et Eric Hintz ont proposé d'y voir l'effet de l'éclipse annulaire du 17 février 478 av. J.-C., dont l'ombre couvrit une grande partie du Péloponnèse ; il s'agit d'une hypothèse<ref>Daniel W. Graham et Eric Hintz, « Anaxagoras and the Solar Eclipse of 478 BC », ''Apeiron'', vol. 40, 2007, p. 319-344 ; Graham, ''Science before Socrates'', 2013 ; Curd et Sisko, ''SEP'', 2026, § 1.</ref>.
La lune, selon Anaxagore, est faite de terre, avec des plaines et des ravins<ref>Hippolyte, ''Réfutation'', I, 8, 10 (DK 59 A 42) ; Diogène Laërce, II, 8 (DK 59 A 1) ; Aétius, II, 30, 2 (DK 59 A 77), qui parle de hauteurs, de plaines et de creux.</ref>. Elle tient sa lumière du soleil<ref>Anaxagore, B18, cité par Plutarque, ''Sur la face qui paraît dans la lune'', 16, 929b (DK 59 B 18) ; Platon, ''Cratyle'', 409a-b (DK 59 A 76).</ref>. Aétius attribue toutefois cette découverte à plusieurs penseurs, depuis Thalès, Pythagore et Parménide ; Daniel Graham en crédite Parménide, et Dirk Couprie a contesté que l'expression « tenir sa lumière du soleil » désigne chez Anaxagore une simple réflexion<ref>Aétius, II, 28, 5 (DK 59 A 77) ; Parménide, DK 28 B 14-15 ; Daniel W. Graham, « La lumière de la lune dans la pensée grecque archaïque », dans André Laks et Claire Louguet (éd.), ''Qu'est-ce que la philosophie présocratique ?'', Lille, Presses universitaires du Septentrion, 2002, p. 351-380 ; Denis O'Brien, « Derived Light and Eclipses in the Fifth Century », ''Journal of Hellenic Studies'', vol. 88, 1968, p. 114-127 ; Dirk L. Couprie, « Anaxagoras on the Light and Phases of the Moon », ''Hyperboreus'', vol. 24, 2018, p. 12-39 ; Curd, 2007, p. 211 et n. 15.</ref>.
Hippolyte crédite Anaxagore d'avoir le premier expliqué les éclipses et les phases de la lune. Une éclipse de lune se produit lorsque la terre s'interpose entre le soleil et la lune ; une éclipse de soleil, lors de la nouvelle lune, lorsque la lune s'interpose entre le soleil et la terre<ref>Hippolyte, ''Réfutation'', I, 8, 9-10 (DK 59 A 42) ; Aétius, II, 29, 6-7 (DK 59 A 77) ; Plutarque, ''Vie de Nicias'', 23 (DK 59 A 18), qui lui attribue le premier exposé écrit de la théorie des phases.</ref>. Cette explication s'accompagne d'un élément étranger à l'astronomie moderne : certaines éclipses de lune seraient dues à des corps invisibles situés au-dessous de la lune. La plupart des historiens y voient une avancée importante de l'astronomie grecque, que Graham et Hintz jugent pour l'essentiel correcte ; Couprie soutient au contraire que l'ombre de la terre servait chez Anaxagore à expliquer la Voie lactée, et que les corps invisibles étaient sa seule explication des éclipses de lune<ref>Hippolyte, ''Réfutation'', I, 8, 6 et 9 (DK 59 A 42) ; Aétius, II, 29, 7 (DK 59 A 77) ; Curd, 2007, p. 211 ; Graham et Hintz, 2007 ; Dirk L. Couprie, « Anaxagoras on the Milky Way and Lunar Eclipses », ''Hyperboreus'', vol. 23, 2017, p. 181-207.</ref>.
La tradition prête aussi à Anaxagore des prédictions, dont celle d'une éclipse de soleil<ref>Philostrate, ''Vie d'Apollonios de Tyane'', I, 2 (DK 59 A 6) ; cf. Hippolyte, ''Réfutation'', I, 8, 13 (DK 59 A 42), qui le dit « devin ».</ref>. Les historiens modernes restent sceptiques à l'égard de ces récits<ref>Curd, 2007, p. 132, n. 9-10.</ref>. L'affirmation que la surface de la lune présente des reliefs analogues à ceux de la terre annonce, à titre d'intuition, ce que les observations de Galilée rendront visible en 1610.
La Voie lactée s'explique, selon Anaxagore, par l'ombre que projette la terre lorsque le soleil passe au-dessous d'elle : les étoiles situées dans cette ombre, que le soleil n'éclaire pas, laissent voir leur propre lumière<ref>Aristote, ''Météorologiques'', I, 8, 345a25-31 (DK 59 A 80) ; Aétius, III, 1, 5 ; Hippolyte, ''Réfutation'', I, 8, 10 (DK 59 A 42) ; Curd, 2007, p. 210 ; Couprie, 2017.</ref>.
=== La météorite d'Aigos Potamos ===
La chute d'une grande météorite à Aigos Potamos, sur la rive européenne de l'Hellespont, vers 467 av. J.-C., contribua à la réputation d'Anaxagore<ref>Marbre de Paros, ép. 57, et Pline l'Ancien, ''Histoire naturelle'', II, 149 (DK 59 A 11) ; Plutarque, ''Vie de Lysandre'', 12 (DK 59 A 12) ; Diogène Laërce, II, 10 (DK 59 A 1). Le Marbre de Paros donne 468/7, Pline la deuxième année de la 78{{e}} Olympiade (467/6).</ref>. Plusieurs sources affirment qu'il l'avait prédite. Burnet jugeait cette prédiction absurde, et Curd rappelle que la chute d'une météorite particulière, en un lieu et à un moment donnés, ne pouvait être prévue<ref>Burnet, 1930, chap. VI ; Curd, 2007, p. 132 et n. 10.</ref>. Selon Plutarque, Anaxagore enseignait que les astres, pierres lourdes maintenues par la tension de la révolution, pouvaient tomber si ce mouvement venait à se relâcher ; Daniel Graham comprend en ce sens que la « prédiction » portait sur la chute de pierres célestes en général, et non sur un événement particulier<ref>Plutarque, ''Vie de Lysandre'', 12 (DK 59 A 12) ; Daniel W. Graham, « Anaxagoras and the Comet », ''Ancient Philosophy'', vol. 33, 2013, p. 1-18 ; Evangelos Th. Theodossiou, P. G. Niarchos, V. N. Manimanis et Wayne Orchiston, « The Fall of a Meteorite at Aegos Potami in 467/466 BC », ''Journal of Astronomical History and Heritage'', vol. 5, 2002, p. 135-140.</ref>. Plutarque rapporte aussi, d'après Daïmachos, qu'un corps enflammé fut observé pendant soixante-quinze jours avant la chute<ref>Plutarque, ''Vie de Lysandre'', 12 (DK 59 A 12) ; Curd, 2007, p. 132.</ref>. Quoi qu'il en soit, l'événement fut associé au nom d'Anaxagore et parut confirmer sa thèse selon laquelle les corps célestes sont faits de pierre.
=== Météorologie ===
Anaxagore expliquait les nuages et la neige à peu près comme Anaximène<ref>Aétius, III, 4, 2 (DK 59 A 85) ; Curd, 2007, p. 223.</ref>. Le fragment B16 décrit une série de solidifications : des nuages se sépare l'eau, de l'eau la terre, et de la terre les pierres, que le froid solidifie<ref>Anaxagore, fragment B16 (DK 59 B 16).</ref>.
La formation de la grêle posait un problème particulier : comment de la glace peut-elle se former en été ? Selon Aristote, qui critique cette théorie, Anaxagore soutenait que la grêle se forme lorsqu'un nuage est poussé vers la région supérieure, plus froide ; les chaleurs de l'été, en poussant les nuages plus haut, expliqueraient la fréquence des orages de grêle en été et dans les pays chauds<ref>Aristote, ''Météorologiques'', I, 12, 348a14-20 et 348b12-16 (DK 59 A 85), avec le commentaire d'Alexandre d'Aphrodise, 49, 13 ; Aétius, III, 4, 2 (DK 59 A 85), qui ajoute que les gouttes s'arrondissent en tombant ; Curd, 2007, p. 223.</ref>. Cette explication présente une analogie avec le rôle que la météorologie moderne attribue aux courants ascendants dans la formation de la grêle, sans qu'il faille lui prêter une théorie de la convection.
Le tonnerre et l'éclair sont produits, selon Anaxagore, lorsque du feu venu de l'éther tombe dans les nuages : l'éclat du feu est l'éclair, et le sifflement du feu qui s'éteint est le tonnerre<ref>Aristote, ''Météorologiques'', II, 9, 369b14-19 (DK 59 A 84) ; Aétius, III, 3, 4 (DK 59 A 84) ; Sénèque, ''Questions naturelles'', II, 12, 3 et II, 19 (DK 59 A 84).</ref>. Aristote précise qu'Anaxagore tenait l'éclair pour réellement antérieur au tonnerre, les choses se passant comme elles apparaissent ; l'explication de ce décalage par la plus grande vitesse de la vue est celle d'Aristote, non celle d'Anaxagore<ref>Aristote, ''Météorologiques'', II, 9, 369b7-19.</ref>.
Les tremblements de terre sont attribués, selon Aristote, à l'éther qui, tendant naturellement vers le haut, se trouve emprisonné dans les cavités souterraines ; Aétius et Hippolyte parlent plutôt d'air qui pénètre sous la terre et l'ébranle<ref>Aristote, ''Météorologiques'', II, 7, 365a19-35 (DK 59 A 89) ; Aétius, III, 15, 4 (DK 59 A 89) ; Hippolyte, ''Réfutation'', I, 8, 12 (DK 59 A 42).</ref>.
=== Hydrologie ===
Anaxagore expliquait la crue estivale du Nil par la fonte des neiges dans les régions montagneuses d'Éthiopie<ref>Hippolyte, ''Réfutation'', I, 8, 5 (DK 59 A 42) ; Aétius, IV, 1, 3 (DK 59 A 91) ; Sénèque, ''Questions naturelles'', IV a, 2, 17 (DK 59 A 91) ; Diodore de Sicile, I, 38, 4 ; Schofield (1980, p. 34) relève qu'Eschyle adopte cette explication dans les ''Suppliantes'' (559-561).</ref>. Hérodote jugeait déjà cette explication la plus séduisante et la plus fausse, et Sénèque la rejette. Elle situe correctement l'origine des eaux dans les hauts plateaux d'Éthiopie, mais les crues sont dues aux pluies de mousson et non à la fonte des neiges<ref>Hérodote, II, 22 (DK 59 A 91) ; Curd, 2007, p. 225.</ref>. Quant à la mer, elle serait née de l'eau stagnante originelle, dont la partie la plus fine s'évapora sous l'effet du soleil, laissant un résidu salé et amer<ref>Aétius, III, 16, 2 (DK 59 A 90) ; Hippolyte, ''Réfutation'', I, 8, 4 (DK 59 A 42) ; Diogène Laërce, II, 8 (DK 59 A 1).</ref>.
=== Synthèse : la cosmologie naturaliste d'Anaxagore ===
La cosmologie d'Anaxagore se caractérise par trois traits.
Le premier est l'unité de la nature : les corps célestes sont faits des mêmes ingrédients que la terre, puisque ce sont des pierres arrachées à celle-ci<ref>Aétius, II, 13, 3 (DK 59 A 71) ; Hippolyte, ''Réfutation'', I, 8, 6 (DK 59 A 42) ; Gershenson et Greenberg, 1964, p. 47.</ref>. Il n'existe pas de différence de nature entre le monde sublunaire et le ciel, contrairement à ce qu'affirmera Aristote<ref>Aristote, ''Du ciel'', I, 2-3, 268b11-270b25.</ref>.
Le deuxième est l'explication physique : une fois la rotation initiée par le Noûs, les phénomènes cosmologiques et météorologiques s'expliquent par des processus tels que la rotation, la séparation selon la densité, l'évaporation ou la condensation, sans recours à des interventions divines<ref>Curd, 2007, p. 222 ; Gershenson et Greenberg, 1964, p. 26.</ref>.
Le troisième est l'usage de l'analogie : Anaxagore explique les phénomènes célestes par analogie avec des phénomènes terrestres observables<ref>Gershenson et Greenberg, 1964, p. 24 et 34.</ref>.
Ces traits donnent à la cosmologie d'Anaxagore un caractère naturaliste marqué, qui représente une étape importante dans l'histoire de l'explication physique de la nature. Il ne faut pas pour autant voir en lui le « fondateur de la méthode scientifique », selon une expression que l'on rencontre parfois ; mieux vaut parler d'un jalon, où se combinent intuitions fécondes et erreurs caractéristiques de son temps<ref>La formule forte est celle de Gershenson et Greenberg (1964, préface), qui font d'Anaxagore le premier savant au sens moderne. Pour une perspective plus prudente, voir G. E. R. Lloyd, ''Early Greek Science: Thales to Aristotle'', Londres, Chatto & Windus, 1970.</ref>.
== La physiologie et la biologie ==
Bien qu'Anaxagore soit surtout connu pour sa cosmologie, les témoignages indiquent qu'il s'intéressa aussi à la nutrition, à la perception, à l'embryologie et aux êtres vivants<ref>Curd, 2007, p. 225-229 ; Gershenson et Greenberg, 1964, p. 55-57.</ref>.
=== La théorie de la nutrition ===
L'une des questions biologiques qu'Anaxagore cherche à élucider est celle de la nutrition : comment une nourriture simple, comme le pain et l'eau, peut-elle nourrir les cheveux, les veines, les artères, la chair, les nerfs et les os ?<ref>Aétius, I, 3, 5 (DK 59 A 46) ; Simplicius, ''Commentaire sur la Physique'', 460, 4-20 (DK 59 A 45) ; Platon, ''Phédon'', 96c-d (DK 59 A 46) ; Lucrèce, ''De la nature'', I, 830-920 (DK 59 A 44).</ref> Cette observation pose un problème au regard de l'interdit parménidien : une substance nouvelle, la chair, semble naître de ce qui n'est pas chair.
La réponse d'Anaxagore est cohérente avec sa métaphysique : le pain doit déjà contenir de la chair, du sang, des os et toutes les autres substances corporelles, en proportions si faibles qu'elles restent imperceptibles<ref>Simplicius, ''Commentaire sur la Physique'', 460, 4-20 (DK 59 A 45), qui ajoute que, si les arbres se nourrissent d'eau, l'eau doit contenir du bois, de l'écorce et des fruits.</ref>. Lorsque nous mangeons, la chair contenue dans la nourriture vient s'ajouter à la chair du corps<ref>Aristote, ''Génération des animaux'', I, 18, 723a10-11.</ref>.
Cette théorie soulève une autre difficulté : comment le corps dirige-t-il la chair vers les muscles et les parties osseuses vers les os ? Les fragments ne le disent pas. Certains interprètes supposent qu'Anaxagore rattachait cette fonction au Noûs présent dans les êtres vivants<ref>Gershenson et Greenberg, 1964, p. 24, pour qui la digestion est, chez Anaxagore, une fonction de l'intellect ; Curd (2007, p. 175-177) suggère que les semences contiennent, par le Noûs ou l'âme, un principe d'organisation, tout en qualifiant cette reconstruction de spéculative.</ref>.
=== La théorie de la perception ===
Anaxagore élabora une théorie de la perception fondée sur le principe que le semblable n'est pas affecté par le semblable : la perception se fait par les contraires<ref>Théophraste, ''Du sens'', 27 (DK 59 A 92).</ref>. Il s'opposait ainsi à Empédocle, pour qui la perception se fait par le semblable<ref>Théophraste, ''Du sens'', 1-2 (DK 31 A 86).</ref>.
Pour qu'une perception ait lieu, il doit exister une différence entre l'organe et l'objet perçu : ce qui est exactement aussi chaud ou aussi froid que nous ne nous réchauffe ni ne nous refroidit à son contact<ref>Théophraste, ''Du sens'', 28 (DK 59 A 92).</ref>. La vision se fait par le reflet de l'objet dans la pupille, qui ne se produit que sur une couleur différente<ref>Théophraste, ''Du sens'', 27 (DK 59 A 92) ; Gershenson et Greenberg, 1964, p. 28-32.</ref>.
Anaxagore en concluait que toute perception s'accompagne de douleur ou de gêne (λύπη), car tout contact du dissemblable produit une irritation, sensible lorsque la sensation est trop longue ou trop intense<ref>Théophraste, ''Du sens'', 29 (DK 59 A 92) ; Aétius, IV, 9, 16, et Aristote, ''Éthique à Nicomaque'', VII, 15, 1154b7-9 (DK 59 A 94) ; James Warren, « Anaxagoras on Perception, Pleasure, and Pain », ''Oxford Studies in Ancient Philosophy'', vol. 33, 2007, p. 19-54 ; Curd, 2007, p. 169 et n. 37.</ref>.
=== La reproduction et l'embryologie ===
Les témoignages sur l'embryologie d'Anaxagore ne concordent pas entièrement. Selon Aristote, il comptait parmi ceux pour qui la différence des sexes est déjà présente dans la semence : la semence vient du mâle, la femelle ne fournissant que le lieu, et les mâles proviennent du côté droit, les femelles du côté gauche<ref>Aristote, ''Génération des animaux'', IV, 1, 763b30-33 (DK 59 A 107) ; Hippolyte, ''Réfutation'', I, 8, 12 (DK 59 A 42). Schofield (1980, p. 34) relève que l'idée selon laquelle la mère ne fournit qu'un lieu apparaît dans les ''Euménides'' d'Eschyle (657-666).</ref>. D'autres sources lui prêtent l'idée d'une contribution séminale des deux parents, l'enfant ressemblant à celui qui a fourni le plus de semence<ref>Censorinus, ''Du jour natal'', 5, 2-4 et 6, 6-8 (DK 59 A 107 et A 111) ; Aétius, V, 7, 4 (DK 59 A 111), qui associe Anaxagore et Parménide ; Curd, 2007, p. 174 et 226-227 ; Oliver Kember, « Anaxagoras' Theory of Sex Differentiation and Heredity », ''Phronesis'', vol. 18, 1973, p. 1-14.</ref>.
=== La génération des animaux et des plantes ===
Selon les témoignages, Anaxagore distinguait la première génération des animaux et leur reproduction ultérieure : les animaux naquirent d'abord de l'humide, du chaud et du terreux, puis les uns des autres<ref>Diogène Laërce, II, 9 (DK 59 A 1) ; Hippolyte, ''Réfutation'', I, 8, 12 (DK 59 A 42).</ref>. Irénée lui attribue l'idée que les animaux sont issus de semences tombées du ciel, et Théophraste celle que l'air contient les semences des plantes, entraînées vers le sol par la pluie<ref>Irénée, ''Contre les hérésies'', II, 14, 2 (DK 59 A 113) ; Théophraste, ''Recherches sur les plantes'', III, 1, 4 (DK 59 A 117) ; Schofield, 1980, p. 125.</ref>.
Anaxagore aurait tenu les plantes pour des animaux attachés au sol<ref>Plutarque, ''Questions naturelles'', 1, 911d (DK 59 A 116).</ref>. Le traité pseudo-aristotélicien ''Sur les plantes'' lui prête l'idée qu'elles éprouvent désir, plaisir et peine, en invoquant la chute de leurs feuilles, et qu'elles possèdent intellect et connaissance<ref>Pseudo-Aristote, ''Sur les plantes'', I, 1, 815a15-20 et 815b16-17 (DK 59 A 117). Schofield (1980, p. 148, n. 40-41) juge difficile d'accorder crédit aux sentiments prêtés aux plantes, mais admet que l'attribution d'une âme et d'un intellect puisse reposer sur une source fiable.</ref>.
=== Observations biologiques diverses ===
Les sources conservent quelques observations isolées. Aristote rapporte qu'Anaxagore et d'autres physiciens prétendaient que le corbeau et l'ibis s'accouplent par le bec et que la belette met bas par la gueule, et il leur reproche d'en parler trop superficiellement et sans examen<ref>Aristote, ''Génération des animaux'', III, 6, 756b13-17 (DK 59 A 114).</ref>. L'erreur sur la belette pourrait venir de l'observation de femelles transportant leurs petits dans la gueule aussitôt après la mise bas<ref>Gershenson et Greenberg, 1964, p. 57.</ref>. Ces erreurs montrent les limites de l'observation dans la biologie d'Anaxagore.
=== L'homme et les animaux ===
Selon Anaxagore, l'homme est le plus intelligent des animaux parce qu'il a des mains ; Aristote renverse la formule et soutient que l'homme a des mains parce qu'il est le plus intelligent<ref>Aristote, ''Parties des animaux'', IV, 10, 687a7-10 (DK 59 A 102) ; Schofield, 1980, p. 16.</ref>. Burnet en tirait l'idée que, l'intellect étant partout le même, les différences d'intelligence entre les êtres vivants dépendent de la structure de leur corps<ref>Burnet, 1930, chap. VI.</ref>.
== L'influence et la postérité ==
Anaxagore occupe une place importante dans l'histoire de la philosophie antique. Premier philosophe de renom établi à Athènes, il y introduit la tradition ionienne de recherche sur la nature<ref>Curd, 2007, p. 129 et 142-146 ; Curd et Sisko, ''SEP'', 2026, § 6.</ref>. Les lignes qui suivent décrivent son influence avec la prudence nécessaire : il est facile, en traitant des présocratiques, de céder à la tentation des grandes filiations, alors que la documentation invite souvent à plus de retenue.
=== La transmission immédiate : Archélaos, Diogène d'Apollonie et le papyrus de Derveni ===
Le premier relais de l'influence d'Anaxagore fut son disciple Archélaos d'Athènes, qu'une partie de la tradition présente comme le maître de Socrate<ref>Diogène Laërce, II, 16 (DK 60 A 1) ; Théophraste, dans Simplicius (DK 60 A 5) ; Curd, 2007, p. 134 et n. 18. Ion de Chios rapporte que le jeune Socrate voyagea à Samos avec Archélaos (DK 60 A 3).</ref>. Clément d'Alexandrie en fait le successeur d'Anaxagore, et Eusèbe précise qu'il lui succéda à la tête de l'école de Lampsaque, indication difficile à concilier avec les témoignages qui situent son enseignement à Athènes<ref>Clément d'Alexandrie, ''Stromates'', I, 63, et Eusèbe, ''Préparation évangélique'', X, 14, 13 (DK 59 A 7) ; Curd, 2007, p. 134, n. 18.</ref>. Archélaos semble avoir prolongé la cosmologie de son maître par un récit des origines des institutions sociales, orientation qui a pu préparer le tournant moral pris ensuite par Socrate<ref>Gábor Betegh, « Archelaus on Cosmogony and the Origins of Social Institutions », ''Oxford Studies in Ancient Philosophy'', vol. 51, 2016, p. 1-40.</ref>.
Diogène d'Apollonie, dans la génération suivante, subit l'influence d'Anaxagore dans sa doctrine comme dans son style ; il fait de l'intelligence un principe cosmique, mais l'attribue à l'air et prend soin de l'appuyer sur des arguments, ce qu'Anaxagore ne faisait guère<ref>Schofield, 1980, p. 5-6 ; Curd, « Presocratic Philosophy », ''SEP'', 2020, § 9.</ref>. L'auteur du papyrus de Derveni, commentaire allégorique d'un poème orphique découvert en 1962, semble avoir repris, en les modifiant, la doctrine d'un Intellect qui gouverne le cosmos et certains aspects de la théorie des ingrédients<ref>Curd, 2007, p. 143 et n. 35 ; Gábor Betegh, ''The Derveni Papyrus: Cosmology, Theology and Interpretation'', Cambridge, Cambridge University Press, 2004, chap. 7.</ref>.
=== L'influence sur Socrate : espoirs et déceptions ===
La relation entre Anaxagore et Socrate passe, dans nos sources, par la lecture du livre. Dans le ''Phédon'', Socrate raconte sa rencontre avec la pensée d'Anaxagore, qui suscita d'abord son enthousiasme, puis sa déception<ref>Platon, ''Phédon'', 97b-98c (DK 59 A 47).</ref>. Comme on l'a vu, cette déception porte sur une attente proprement socratique, celle d'une explication par le meilleur, qu'Anaxagore n'avait pas formulée dans les termes que Socrate aurait voulus.
Le ''Phédon'' présente ensuite le passage de Socrate à une autre méthode de recherche des causes, fondée sur les Formes, que Socrate lui-même présente comme une « seconde navigation »<ref>Platon, ''Phédon'', 99c-100a ; David Sedley, « Teleology and Myth in the ''Phaedo'' », ''Proceedings of the Boston Area Colloquium in Ancient Philosophy'', vol. 5, 1989, p. 359-383 ; Curd, 2007, p. 136, n. 20, et p. 144-145.</ref>. L'idée qu'un principe intelligent gouverne l'univers, que Socrate trouve chez Anaxagore et juge insuffisamment exploitée, a pu orienter sa recherche sans qu'il faille faire d'Anaxagore sa cause nécessaire.
Dans l’''Apologie'', l'accusation portée contre Socrate inclut des thèses cosmologiques qui sont en réalité celles d'Anaxagore, ce qui montre l'association des deux penseurs dans l'esprit des Athéniens, indépendamment de la distance qui les sépare<ref>Platon, ''Apologie de Socrate'', 26d-e (DK 59 A 35) ; Curd, 2007, p. 142-143.</ref>. Xénophon présente de son côté Socrate mettant ses disciples en garde contre l'étude des phénomènes célestes à la manière d'Anaxagore, qu'il accuse d'avoir perdu la raison en identifiant le soleil au feu<ref>Xénophon, ''Mémorables'', IV, 7, 6-7 (DK 59 A 73).</ref>.
=== L'appropriation platonicienne : du Noûs au Démiurge ===
Platon reprend la doctrine du Noûs et la transforme en une cosmologie téléologique. Dans le ''Timée'', le Démiurge, artisan divin qui façonne le monde sensible en contemplant les Formes, peut se lire en dialogue avec le Noûs d'Anaxagore<ref>Platon, ''Timée'', 29a-30c et 47e-48a ; Curd, 2007, p. 145 ; Schofield, 1980, p. 61 ; Glenn R. Morrow, « Necessity and Persuasion in Plato's ''Timaeus'' », ''Philosophical Review'', vol. 59, 1950, p. 147-163 ; Luc Brisson, ''Le Même et l'Autre dans la structure ontologique du Timée de Platon'', Paris, Klincksieck, 1974.</ref>. La téléologie platonicienne va cependant bien au-delà de ce qu'affirmait Anaxagore : le Démiurge façonne le monde en vue du meilleur en contemplant des réalités intelligibles, arrière-plan qui n'a pas d'équivalent dans les fragments. Schofield note toutefois un point commun : pour Anaxagore comme pour Platon, c'est l'ordre du monde qui fait problème, non l'existence des choses, et la matière est présupposée<ref>Schofield, 1980, p. 61.</ref>.
Dans le ''Philèbe'', Socrate rappelle l'accord des sages pour dire que l'Intellect est roi du ciel et de la terre<ref>Platon, ''Philèbe'', 28c ; cf. ''Cratyle'', 413c (DK 59 A 55), où la justice est identifiée à l'Intellect « selon Anaxagore ».</ref>. Dans les ''Lois'', Platon évoque ceux qui ont osé dire que c'est l'Intellect qui a ordonné tout ce qui est dans le ciel, allusion probable à Anaxagore<ref>Platon, ''Lois'', XII, 967b-c.</ref>. Curd montre en outre que Platon réemploie le vocabulaire anaxagoréen : les Formes sont, comme le Noûs, « elles-mêmes par elles-mêmes », et les choses sensibles « participent » des Formes comme, chez Anaxagore, les choses ont part à toutes choses<ref>Curd, 2007, p. 50 et 143-145.</ref>. L'influence d'Anaxagore sur Platon est donc réelle, mais elle passe par une transformation profonde : le Noûs, principe moteur et cognitif, devient chez Platon un Intellect qui façonne le monde selon le meilleur ordre possible.
=== La critique aristotélicienne ===
Aristote, tout en reconnaissant l'importance historique d'Anaxagore, en fut l'un des critiques les plus sévères. Dans la ''Métaphysique'', il dit qu'Anaxagore parut comme un homme sobre au milieu de prédécesseurs qui parlaient au hasard<ref>Aristote, ''Métaphysique'', A, 3, 984b15-20 (DK 59 A 58).</ref>, avant de lui reprocher de se servir de l'Intellect comme d'un ''deus ex machina''<ref>Aristote, ''Métaphysique'', A, 4, 985a18-21 (DK 59 A 47).</ref>. Ses critiques portent aussi, dans le traité ''De l'âme'', sur la confusion entre l'âme et l'intellect et sur la manière dont le Noûs connaît<ref>Aristote, ''De l'âme'', I, 2 et III, 4 (DK 59 A 55, A 99, A 100) ; Curd, 2007, p. 146.</ref>.
Aristote décrit par ailleurs la doctrine d'Anaxagore au moyen de son propre vocabulaire, notamment le terme d'homéomère (ὁμοιομερής), qu'Anaxagore n'a probablement jamais employé<ref>Curd, 2007, p. 147-150 ; Schofield, 1980, p. 128-132.</ref>. La distinction entre parties homéomères (chair, os, sang) et anhoméomères (main, visage) appartient à la biologie d'Aristote lui-même ; il s'en sert pour décrire rétrospectivement la doctrine d'Anaxagore<ref>Aristote, ''Parties des animaux'', II, 1, 646a12-24 ; ''Génération et corruption'', I, 1, 314a18-20 (DK 59 A 46).</ref>.
=== L'héritage dans la philosophie hellénistique et tardive ===
Après Aristote, la pensée d'Anaxagore continua d'exercer une influence diffuse. La doctrine stoïcienne d'une raison divine immanente qui gouverne la nature présente des analogies avec le Noûs, mais les Stoïciens, matérialistes, rejettent la séparation de l'intellect et des corps, et aucune dépendance directe n'est établie<ref>A. A. Long et D. N. Sedley, ''The Hellenistic Philosophers'', vol. I, Cambridge, Cambridge University Press, 1987 ; Michael J. White, « Stoic Natural Philosophy (Physics and Cosmology) », dans Brad Inwood (éd.), ''The Cambridge Companion to the Stoics'', Cambridge, Cambridge University Press, 2003, p. 124-152.</ref>.
Du côté épicurien, Lucrèce critique longuement l’''homoeomeria'' d'Anaxagore<ref>Lucrèce, ''De la nature'', I, 830-920 (DK 59 A 44) ; David Sedley, ''Lucretius and the Transformation of Greek Wisdom'', Cambridge, Cambridge University Press, 1998.</ref>, et Cicéron fait railler par l'épicurien Velléius son Intellect illimité<ref>Cicéron, ''De la nature des dieux'', I, 11, 26 (DK 59 A 48).</ref>. Selon Dioclès, cité par Diogène Laërce, Épicure approuvait pourtant Anaxagore plus que tout autre ancien, tout en s'en écartant sur certains points<ref>Diogène Laërce, X, 12 (DK 59 A 26).</ref>.
Dans la tradition néoplatonicienne, Anaxagore est lu à travers Platon et Aristote ; les commentateurs formulent ses principes du « tout en tout » et de la prédominance dans leur propre vocabulaire<ref>Proclus, ''Éléments de théologie'', 103 ; Schofield, 1980, p. 155, n. 52.</ref>. Simplicius, au VI{{e}} siècle, consacre de longs passages de son commentaire sur la ''Physique'' d'Aristote à Anaxagore : c'est à lui que nous devons la plupart des fragments<ref>Simplicius, ''Commentaire sur la Physique'', passim ; Teodorsson, 1982, p. 10.</ref>.
=== Un héritage à ne pas surinterpréter ===
Il est tentant de présenter Anaxagore comme un précurseur de la science mécaniste moderne, de l'atomisme ou de la physique contemporaine. Il faut résister à cette tentation. Au XVII{{e}} siècle, certains philosophes mécanistes ont invoqué les présocratiques, mais c'est l'atomisme de Leucippe et de Démocrite, transmis par Épicure et Lucrèce, qui a nourri la physique corpusculaire de Gassendi, de Boyle ou de Newton. L'influence propre d'Anaxagore sur cette tradition reste limitée et indirecte : sa doctrine de la divisibilité illimitée et du mélange universel s'oppose aux postulats de l'atomisme<ref>Alan Chalmers, « Atomism from the 17th to the 20th Century », ''Stanford Encyclopedia of Philosophy'', 2005 (révisions ultérieures) ; Andrew Pyle, ''Atomism and its Critics: From Democritus to Newton'', Bristol, Thoemmes Press, 1995.</ref>.
On rencontre aussi des rapprochements entre le « tout dans tout » et certaines idées de la physique moderne (théorie des champs, intrication quantique, non-séparabilité). Ces rapprochements, parfois suggérés par des physiciens eux-mêmes, peuvent avoir une valeur pédagogique ; ils ne doivent pas être transformés en affirmations de continuité doctrinale. La physique quantique procède d'un appareil mathématique et expérimental sans équivalent chez les Grecs, et les analogies qu'on peut tracer avec Anaxagore relèvent de la métaphore rétrospective<ref>Werner Heisenberg, ''Physics and Philosophy'', New York, Harper, 1958, évoque les présocratiques sans établir de filiation précise avec la physique quantique.</ref>.
On a parfois suggéré que les théologiens médiévaux auraient vu dans le Noûs un précurseur du Dieu créateur. La prudence s'impose : Thomas d'Aquin, Maïmonide et Avicenne s'inscrivent dans une tradition aristotélicienne et néoplatonicienne déjà élaborée, où le Noûs d'Anaxagore n'apparaît, quand il est mentionné, qu'à travers Aristote, et le plus souvent pour être critiqué. Le Noûs, qui ordonne un mélange qu'il ne crée pas, ne se confond d'ailleurs pas avec un Dieu créateur<ref>Schofield, 1980, p. 61 ; Curd, 2007, p. 61 et 201, n. 17 ; Étienne Gilson, ''L'Esprit de la philosophie médiévale'', Paris, Vrin, 1932 ; Cristina D'Ancona, « Greek into Arabic: Neoplatonism in Translation », dans Peter Adamson et Richard C. Taylor (éd.), ''The Cambridge Companion to Arabic Philosophy'', Cambridge, Cambridge University Press, 2005, p. 10-31.</ref>.
Une dernière mise en garde concerne la prédiction d'éclipses et la chute de la météorite d'Aigos Potamos, parfois présentées comme des triomphes d'une « méthode scientifique » naissante. Les sources qui attribuent à Anaxagore ces prédictions sont tardives et souvent légendaires ; les historiens se montrent prudents, voire sceptiques<ref>Curd, 2007, p. 132 et n. 9-10 ; Burnet, 1930, chap. VI ; Graham, « Anaxagoras and the Comet », 2013.</ref>. L'importance d'Anaxagore ne tient pas à la précision de ses prédictions, mais à la cohérence et à l'ambition explicative de son système naturaliste, qui rend compte des phénomènes célestes par des principes physiques accessibles à la raison.
=== Bilan ===
La réception d'Anaxagore est marquée par un contraste. D'un côté, Aristote salue l'introduction du Noûs comme principe cosmique et loue sa sobriété ; de l'autre, Socrate, Platon et Aristote jugent insuffisant l'usage qu'il en fait. Anaxagore apparaît ainsi à la fois comme le penseur qui a rendu pensable une cause intellectuelle du cosmos et comme celui qui, au goût de ses successeurs, l'a laissée inemployée<ref>Curd, 2007, p. 142-146 et 204-205 ; Schofield, 1980, p. 59-61 ; Graham, ''Science before Socrates'', 2013.</ref>.
Cette limite même a été féconde. Socrate se tourne vers la recherche de causes que le Noûs anaxagoréen ne fournissait pas. Platon développe, dans le ''Timée'', une cosmologie téléologique où le Démiurge façonne le monde en contemplant les Formes. Aristote élabore, contre Platon autant que contre Anaxagore, une doctrine des causes qui intègre la cause finale. Tous trois ont puisé chez Anaxagore, au prix d'une transformation qui leur appartient ; leurs doctrines ont leurs propres fondements, et se construisent souvent en réaction à Anaxagore autant qu'en prolongement de ses thèses.
Sur le plan scientifique, l'héritage d'Anaxagore est surtout celui d'un modèle d'explication naturaliste. Le soleil n'est plus Hélios mais une pierre incandescente ; la lune n'est plus une déesse mais un corps terreux qui reçoit sa lumière du soleil ; la foudre n'est plus le trait de Zeus mais un phénomène atmosphérique. Cet héritage a nourri durablement la tradition philosophique et scientifique grecque. Il serait pourtant excessif de lui attribuer la fondation d'une « méthode scientifique » au sens moderne : celle-ci suppose la preuve démonstrative, la mathématisation de la nature et bien d'autres étapes que la pensée d'Anaxagore n'a pas franchies<ref>Gershenson et Greenberg (1964) défendent une version forte de la thèse d'Anaxagore fondateur de la méthode scientifique, à nuancer ; cf. Lloyd, 1970.</ref>.
Anaxagore occupe en définitive une place singulière : celle d'un pionnier qui ouvre une voie sans la parcourir entièrement, et qui lègue à ses successeurs à la fois une doctrine et la tâche de penser ce qu'elle laisse ouvert. Placé entre la cosmologie ionienne, dont il est le dernier grand représentant à Athènes, et la philosophie classique, qu'il contribue à préparer, il mérite d'être étudié pour lui-même, et non seulement à travers les influences qu'il a exercées.
== Notes et références ==
{{references|colonnes=2}}
== Bibliographie ==
=== Textes anciens : éditions et traductions ===
; Diels, Hermann & Kranz, Walther (éds.)
: ''Die Fragmente der Vorsokratiker'', 3 vol., Berlin, Weidmann, 1951-1952 (6{{e}} éd.)
: [Édition standard de référence pour les fragments présocratiques, avec le système de numérotation DK (Diels-Kranz) utilisé dans la présente étude]
; Laks, André & Most, Glenn W. (éds.)
: ''Early Greek Philosophy'', 9 vol., Loeb Classical Library, Cambridge (Mass.), Harvard University Press, 2016
: [Édition plus récente avec traduction anglaise et nouveau système de numérotation, utile en complément de Diels-Kranz]
; Kirk, G. S., Raven, J. E. & Schofield, M. (éds.)
: ''The Presocratic Philosophers: A Critical History with a Selection of Texts'', 2{{e}} édition, Cambridge, Cambridge University Press, 1983
: [Traductions anglaises commentées des fragments]
; Curd, Patricia (éd.)
: ''Anaxagoras of Clazomenae: Fragments and Testimonia'', Toronto, University of Toronto Press, 2007
: [Édition critique avec traductions anglaises commentées et analyses détaillées]
; Sider, David (éd.)
: ''The Fragments of Anaxagoras: With a Commentary'', Sankt Augustin, Academia Verlag, 2005
: [Édition avec commentaire détaillé]
; Platon
: ''Phédon'', trad. fr. Monique Dixsaut, Paris, GF-Flammarion, 1991
: [Dialogue contenant la critique socratique du Noûs d'Anaxagore]
; Platon
: ''Apologie de Socrate'', trad. fr. Luc Brisson, Paris, GF-Flammarion, 1997
; Platon
: ''Timée'', trad. fr. Luc Brisson, Paris, GF-Flammarion, 1992
; Platon
: ''Philèbe'', trad. fr. Alfred Diès, Paris, Les Belles Lettres, 1941
; Platon
: ''Lois'', trad. fr. Édouard des Places, Paris, Les Belles Lettres, 1951-1956
; Aristote
: ''Métaphysique'', trad. fr. Jean Tricot, Paris, Vrin, 1933
; Aristote
: ''Physique'', trad. fr. Jean Tricot, Paris, Vrin, 1936
; Aristote
: ''Du Ciel'', trad. fr. Jean Tricot, Paris, Vrin, 1949
; Aristote
: ''Génération et corruption'', trad. fr. Jean Tricot, Paris, Vrin, 1950
; Aristote
: ''Génération des animaux'', trad. fr. Jean Tricot, Paris, Vrin, 1957
; Aristote
: ''Histoire des animaux'', trad. fr. Jean Tricot, Paris, Vrin, 1957
; Aristote
: ''Parties des animaux'', trad. fr. Jean Tricot, Paris, Vrin, 1957
; Aristote
: ''De l'âme'', trad. fr. Richard Bodéüs, Paris, GF-Flammarion, 1993
; Diogène Laërce
: ''Vies et doctrines des philosophes illustres'', sous la dir. de Marie-Odile Goulet-Cazé, Paris, Le Livre de Poche (La Pochothèque), 1999
; Théophraste
: ''De Sensibus'' (''On Sense Perception''), trad. angl. dans Kirk, Raven & Schofield, 1983
; Simplicius
: ''In Aristotelis Physicorum libros commentaria'', éd. Hermann Diels, Berlin, Reimer, 1882-1895
: [Source principale pour la transmission des fragments d'Anaxagore]
; Hippolyte
: ''Réfutation de toutes les hérésies'', trad. angl. dans Curd, 2007
; Lucrèce
: ''De la nature'', trad. fr. Alfred Ernout, Paris, Les Belles Lettres, 1920
; Cicéron
: ''De la nature des dieux'', trad. fr. Clara Auvray-Assayas, Paris, Les Belles Lettres, 2002
; Plutarque
: ''Vies parallèles'' (''Vie de Périclès'', ''Vie de Lysandre''), trad. fr. Anne-Marie Ozanam, Paris, Gallimard, Bibliothèque de la Pléiade, 2001
; Xénophon
: ''Mémorables'', trad. fr. Louis-André Dorion et Michele Bandini, Paris, Les Belles Lettres, 2000-2011
=== Études modernes : histoire et philosophie antiques ===
; Schofield, Malcolm
: ''An Essay on Anaxagoras'', Cambridge, Cambridge University Press, 1980
: [Monographie majeure : étude exhaustive de la pensée anaxagoréenne avec analyse textuelle détaillée]
; Curd, Patricia
: « Anaxagoras », ''Stanford Encyclopedia of Philosophy'', publié 2007, révision substantielle 2019
: [Synthèse de référence, avec bibliographie à jour]
; Guthrie, W. K. C.
: ''A History of Greek Philosophy'', vol. II : ''The Presocratic Tradition from Parmenides to Democritus'', Cambridge, Cambridge University Press, 1965
; Barnes, Jonathan
: ''The Presocratic Philosophers'', 2 vol., Londres, Routledge, 1982 (révisé 2006)
; Kirk, G. S., Raven, J. E. & Schofield, M.
: ''The Presocratic Philosophers: A Critical History with a Selection of Texts'', 2{{e}} édition, Cambridge, Cambridge University Press, 1983
; Vlastos, Gregory
: « The Physical Theory of Anaxagoras », ''Philosophical Review'', vol. 59, 1950, p. 31-57
; Peck, Arthur L.
: « Anaxagoras: Predication as a Problem in Physics », ''Classical Quarterly'', vol. 25, 1931, p. 27-37 et p. 112-120
; Strang, Colin
: « The Physical Theory of Anaxagoras », ''Archiv für Geschichte der Philosophie'', vol. 45, 1963, p. 101-118
; Cornford, F. M.
: « Anaxagoras' Theory of Matter », ''Classical Quarterly'', vol. 24, 1930, p. 14-30
; Graham, Daniel W.
: « Was Anaxagoras a Reductionist? », ''Ancient Philosophy'', vol. 24, 2004, p. 1-18
; Graham, Daniel W.
: ''Science before Socrates: Parmenides, Anaxagoras, and the New Astronomy'', Oxford, Oxford University Press, 2006
; Curd, Patricia
: ''The Legacy of Parmenides: Eleatic Monism and Later Presocratic Thought'', Princeton, Princeton University Press, 1998
; Owen, G. E. L.
: « The Place of the Timaeus in Plato's Dialogues », ''Classical Quarterly'', vol. 3, 1953, p. 79-95 ; repris dans ''Logic, Science and Dialectic'', Londres, Duckworth, 1986
; Raven, J. E.
: « The Basis of Anaxagoras' Cosmology », ''Classical Quarterly'', vol. 4, 1954, p. 123-137
; Kerferd, George B.
: « Anaxagoras and the Concept of Matter before Aristotle », ''Bulletin of the John Rylands Library'', vol. 52, 1969, p. 129-143
; Lloyd, David
: « Anaxagoras on Life and Mind », ''Phronesis'', vol. 14, 1969, p. 246-251
; Deichgräber, Karl
: « Anaxagoras Stil », ''Philologus'', vol. 87, 1933, p. 15-26
; Norden, Eduard
: ''Agnostos Theos: Untersuchungen zur Formengeschichte religiöser Rede'', Leipzig, Teubner, 1913
; Taylor, A. E.
: « On the Date of the Trial of Anaxagoras », ''Classical Quarterly'', vol. 11, 1917, p. 81-87
; Kahn, Charles H.
: ''Anaximander and the Origins of Greek Cosmology'', New York, Columbia University Press, 1960
; Inwood, Brad
: « Anaxagoras and Infinite Divisibility », ''Illinois Classical Studies'', vol. 11, 1986, p. 17-33
; Sedley, David
: ''Creationism and Its Critics in Antiquity'', Berkeley, University of California Press, 2007
; Sedley, David
: « Teleology and Myth in the ''Phaedo'' », ''Proceedings of the Boston Area Colloquium in Ancient Philosophy'', vol. 5, 1989, p. 359-383
; Betegh, Gábor
: « Archelaus on Cosmogony and the Origins of Social Institutions », ''Oxford Studies in Ancient Philosophy'', vol. 51, 2016, p. 1-40
; Sider, David
: « Anaxagoras, Socrates, and the History of "Philosophy" », ''Research Bulletin of the Center for Hellenic Studies'', 31 octobre 2016
; Furley, David L.
: ''Two Studies in the Greek Atomists'', Princeton, Princeton University Press, 1967
; Furley, David L.
: « Anaxagoras, Plato and Naming of Parts », dans ''Presocratic Philosophy: Essays in Honour of Alexander Mourelatos'', éd. Victor Caston et Daniel Graham, Aldershot, Ashgate, 2002, p. 119-126
; Furth, Montgomery
: « A Philosophical Hero? Anaxagoras and the Eleatics », ''Oxford Studies in Ancient Philosophy'', vol. 9, 1991, p. 95-129
; Lesher, James
: « Mind's Knowledge and Powers of Control in Anaxagoras », ''Phronesis'', vol. 40, 1995, p. 125-142
; Marmodoro, Anna
: « Anaxagoras's Qualitative Gunk », ''British Journal for the History of Philosophy'', vol. 23, 2015, p. 402-422
; Marmodoro, Anna & Morison, Benjamin (éds.)
: ''Everything in Everything: Anaxagoras's Metaphysics'', Oxford, Oxford University Press, 2019
; Vassallo, Christian
: « Nous, Motion, and Teleology in Anaxagoras », ''Oxford Studies in Ancient Philosophy'', vol. 50, 2016, p. 1-32
=== Études modernes : biologie, physiologie et philosophie naturelle ===
; Gershenson, Daniel E. & Greenberg, Daniel A.
: ''Anaxagoras and the Birth of Scientific Method'', New York, Blaisdell, 1964
: [À utiliser avec précaution : la thèse centrale sur la « naissance de la méthode scientifique » doit être tempérée]
; Meyer, Arthur William
: ''Essays on the History of Embryology'', Stanford, Stanford University Press, 1939
; Preus, Anthony
: « The Techne of Nutrition in Ancient Greek Philosophy », ''Apeiron'', vol. 53, 2020, p. 97-124
; Kember, Oliver
: « Anaxagoras' Theory of Sex Differentiation and Heredity », ''Phronesis'', vol. 18, 1973, p. 1-14
; Mittwoch, Ursula
: « Sex Determination: Science & Society Series on Sex and Science », ''EMBO Reports'', vol. 14, 2013, p. 588-592
; Kupreeva, Inna
: « Sensing the World: Humans, Plants, and the Physicality of Life in Early Greek Philosophy », dans ''Physis and Psyche in Plato and Aristotle'', éd. S. D. Kolstrup et T. L. Kind, Londres, Bloomsbury, 2024, p. 95-114
=== Études modernes : astronomie, cosmologie et météorologie ===
; Theodossiou, Evangelos Th., Dimitrijevic, Milcho S., Mantarakis, Nikos A. & Georgakarakos, Nikolaos I.
: « The Fall of a Meteorite at Aegos Potami in 467/6 BC », ''Journal of Astronomical History and Heritage'', vol. 5, 2002, p. 135-140
; Graham, Daniel W. & Hintz, Eric
: « Anaxagoras and the Comet », ''Apeiron'', vol. 40, 2007, p. 1-20
; Couprie, Dirk L.
: « Anaxagoras on the Milky Way and Lunar Eclipses », ''Rhizomata'', vol. 5, 2017, p. 127-147
; Lloyd, Geoffrey E. R.
: ''Early Greek Science: Thales to Aristotle'', Londres, Chatto & Windus, 1970
=== Études modernes : influence et réception ===
; Vlastos, Gregory
: ''Socrates: Ironist and Moral Philosopher'', Ithaca, Cornell University Press, 1991
; Morrow, Glenn R.
: « Necessity and Persuasion in Plato's ''Timaeus'' », ''Philosophical Review'', vol. 59, 1950, p. 147-163
; Brisson, Luc
: ''Le Même et l'Autre dans la structure ontologique du Timée de Platon'', Paris, Klincksieck, 1974
; Johansen, Thomas Kjeller
: ''Plato's Natural Philosophy'', Cambridge, Cambridge University Press, 2004
; Johansen, Thomas Kjeller
: « From Craft to Nature: The Emergence of Natural Teleology », dans ''Plato and Hesiod'', éd. G. R. Boys-Stones et J. H. Haubold, Oxford, Oxford University Press, 2020, p. 100-125
; Long, A. A. & Sedley, D. N.
: ''The Hellenistic Philosophers'', vol. I, Cambridge, Cambridge University Press, 1987
; White, Michael J.
: « Stoic Natural Philosophy », dans ''The Cambridge Companion to the Stoics'', éd. Brad Inwood, Cambridge, Cambridge University Press, 2003, p. 124-152
; Sedley, David
: ''Lucretius and the Transformation of Greek Wisdom'', Cambridge, Cambridge University Press, 1998
; Chalmers, Alan
: « Atomism from the 17th to the 20th Century », ''Stanford Encyclopedia of Philosophy'', 2005 (révisé 2014)
; Pyle, Andrew
: ''Atomism and its Critics: From Democritus to Newton'', Bristol, Thoemmes Press, 1997
; Gilson, Étienne
: ''L'esprit de la philosophie médiévale'', Paris, Vrin, 1932
; Heisenberg, Werner
: ''Physics and Philosophy'', New York, Harper, 1958
: [À consulter pour les rapprochements prudents entre présocratiques et physique moderne, à titre d'analogie heuristique]
=== Instruments de recherche et ressources ===
; Curd, Patricia
: « Anaxagoras », ''Stanford Encyclopedia of Philosophy'', 2007 (révision substantielle 2019)
; Graham, Daniel W.
: « Presocratic Philosophy », ''Stanford Encyclopedia of Philosophy'', 2019
; ''Oxford Classical Dictionary''
: s.v. « Anaxagoras », diverses éditions
: [À consulter pour la mise en garde prudente sur la tradition biographique]
=== Dictionnaires et encyclopédies ===
; Goulet, Richard (éd.)
: ''Dictionnaire des philosophes antiques'', vol. I, Paris, CNRS Éditions, 1989 (2{{e}} éd. 2003)
: [Entrée détaillée sur Anaxagore avec bibliographie]
{{autocat}}
[[Catégorie:Philosophe]]
{{DEFAULTSORT:Anaxagore}}
[[Catégorie:Présocratiques]]
o71vpr9lmuier9qwx1933tg8xdop0tt
773219
773218
2026-09-26T06:57:11Z
PandaMystique
119061
773219
wikitext
text/x-wiki
{{DicoPhilo|Anaxagore de Clazomènes|lecture=oui}}
== Vie et contexte historique ==
{{wikisource|Auteur:Anaxagore_de_Clazomènes|Anaxagore de Clazomènes}}
Les données biographiques concernant Anaxagore sont, selon l'expression de Patricia Curd, « confuses et déroutantes » (''confused and confusing''), et la plupart des anecdotes transmises par la tradition antique doivent être abordées avec une prudence critique<ref>Patricia Curd, ''Anaxagoras of Clazomenae: Fragments and Testimonia'', Toronto, University of Toronto Press, 2007, p. 130. Curd rappelle que les chroniqueurs anciens cherchaient moins l'exactitude que les synchronismes et les filiations de maître à disciple.</ref>. Les récits qui nous sont parvenus proviennent pour l'essentiel de sources tardives : Plutarque (I{{er}}-II{{e}} siècle ap. J.-C.) dans ses ''Vies parallèles'', Valère Maxime (I{{er}} siècle ap. J.-C.) dans ses ''Faits et dits mémorables'', puis Diogène Laërce (III{{e}} siècle ap. J.-C.) dans ses ''Vies et doctrines des philosophes illustres''. Ces auteurs compilent des traditions antérieures (Démétrios de Phalère, Apollodore, Sotion, Satyros, Hermippe), parfois stylisées à des fins rhétoriques ou morales, parfois légendaires.
Anaxagore (en grec ancien Ἀναξαγόρας, nom formé sur ἄναξ, « seigneur », et ἀγορά, « assemblée ») naît vers 500 av. J.-C. à Clazomènes, cité grecque d'Ionie située sur la côte occidentale de l'actuelle Turquie, à une trentaine de kilomètres à l'ouest d'Izmir<ref>Diogène Laërce, II, 7 (DK 59 A 1) : selon Apollodore, il naquit lors de la 70{{e}} Olympiade (500-497 av. J.-C.) et mourut la première année de la 88{{e}} (428/7). La date de naissance vers 500 fait l'objet d'un large accord : Curd, 2007, p. 130 ; Malcolm Schofield, ''An Essay on Anaxagoras'', Cambridge, Cambridge University Press, 1980, p. 33 ; David Sider, ''The Fragments of Anaxagoras'', Meisenheim am Glan, Hain, 1981, p. 1-2.</ref>. Fils d'Hégésibule (certaines sources disent Eubule), il appartient, selon la tradition, à une famille aristocratique et fortunée<ref>Diogène Laërce, II, 6 (DK 59 A 1).</ref>. Diogène Laërce rapporte qu'il céda son patrimoine à ses proches et qu'il se consacra à l'étude de la nature en se tenant à l'écart des affaires publiques ; à ceux qui lui reprochaient de négliger ses biens, il aurait répondu : « Pourquoi ne vous en occupez-vous pas vous-mêmes ? »<ref>Diogène Laërce, II, 6-7 (DK 59 A 1). Platon, ''Hippias majeur'', 283a, et Plutarque, ''Vie de Périclès'', 16, évoquent aussi la négligence d'Anaxagore envers son héritage (DK 59 A 13).</ref>. Valère Maxime rapporte de son côté qu'Anaxagore, revenant d'un long voyage et trouvant ses terres à l'abandon, aurait déclaré : « Je ne serais pas sauf si elles n'avaient pas péri »<ref>Valère Maxime, ''Faits et dits mémorables'', VIII, 7, ext. 6 (DK 59 A 31). Diogène Laërce ne rapporte pas ce mot. Valère Maxime y voit une parole empreinte de sagesse.</ref>.
Ces récits, qu'ils soient authentiques ou légendaires, illustrent l'image qu'Anaxagore a laissée : celle du philosophe contemplatif, détaché des affaires domestiques et tourné vers l'étude du cosmos. À quelqu'un qui lui demandait s'il n'avait aucun souci de sa patrie, il aurait répondu, en montrant le ciel : « Tais-toi, j'ai le plus grand souci de ma patrie »<ref>Diogène Laërce, II, 7 (DK 59 A 1).</ref>. Interrogé sur l'homme le plus heureux, il aurait dit qu'aucun de ceux auxquels on songe ne mérite ce titre, et que l'homme heureux paraîtrait étrange à son interlocuteur<ref>Aristote, ''Éthique à Eudème'', I, 4, 1215b6-8 ; cf. ''Éthique à Nicomaque'', X, 9, 1179a13-16 (DK 59 A 30).</ref>. À qui lui demandait pourquoi l'on devrait choisir de naître plutôt que de ne pas être, il aurait répondu : « Pour contempler le ciel et l'ordre de l'univers entier »<ref>Aristote, ''Éthique à Eudème'', I, 5, 1216a11-14 (DK 59 A 30). Schofield (1980, p. 22-23) rapproche ces anecdotes du portrait platonicien du philosophe dans la digression du ''Théétète'' et en souligne le caractère stylisé.</ref>.
=== L'arrivée à Athènes et l'activité philosophique ===
La chronologie de la vie d'Anaxagore reste discutée, en raison des divergences entre les sources anciennes<ref>Leonard Woodbury, « Anaxagoras and Athens », ''Phoenix'', vol. 35, 1981, p. 295-315 ; Jaap Mansfeld, « The Chronology of Anaxagoras' Athenian Period and the Date of His Trial », ''Mnemosyne'', vol. 32, 1979, p. 39-69, et vol. 33, 1980, p. 17-95 ; Schofield, 1980, p. 33-35 ; Sider, 1981, p. 1-11.</ref>. Diogène Laërce rapporte, d'après Démétrios de Phalère, qu'Anaxagore commença à philosopher à Athènes à l'âge de vingt ans, sous l'archontat de Callias, et qu'il y séjourna trente ans ; il le dit aussi âgé de vingt ans lors du passage de Xerxès (480 av. J.-C.)<ref>Diogène Laërce, II, 7 (DK 59 A 1). Le nom de l'archonte pose problème : Callias correspond à 456/5, tandis que la correction « Calliadès » (480/79) s'accorde avec l'âge de vingt ans. Voir Sider, 1981, p. 2-4 ; Curd, 2007, p. 78, n. 1, et p. 131.</ref>. Une arrivée précoce, vers 480, est défendue par Leonard Woodbury et Daniel Graham ; Jaap Mansfeld retient 456/5 et un séjour de vingt ans, date qu'Olof Gigon admettait déjà et que suit Curd ; d'autres proposent des dates intermédiaires<ref>Woodbury, 1981, p. 299 et 306 ; Olof Gigon, « Zu Anaxagoras », ''Philologus'', vol. 91, 1936-1937, p. 1-41, repris dans ''Studien zur antiken Philosophie'', Berlin et New York, De Gruyter, 1972 ; Daniel W. Graham, ''Explaining the Cosmos: The Ionian Tradition of Scientific Philosophy'', Princeton, Princeton University Press, 2006 ; Mansfeld, 1979, p. 39 ; Curd, 2007, p. 131. Pour une présentation récente des hypothèses (480, 478, 464, 460, 456), voir Patricia Curd et John Sisko, « Anaxagoras », ''Stanford Encyclopedia of Philosophy'', 2007, révision substantielle 2026, § 1.</ref>. Les sources attribuent au séjour athénien une durée de vingt ou de trente ans ; selon la chronologie retenue, on le situe approximativement entre 480 et 450 ou entre 456 et 437 av. J.-C. Schofield, de son côté, place la composition du livre vers 470-460 av. J.-C.<ref>Curd, 2007, p. 129 ; Schofield, 1980, p. 33-35.</ref>. Aristote dit d'Anaxagore qu'il était antérieur à Empédocle par l'âge, mais postérieur par ses œuvres, formule dont le sens est discuté ; Denis O'Brien a soutenu, en s'appuyant sur Alcidamas, selon qui Empédocle avait été l'auditeur d'Anaxagore, que celui-ci écrivit le premier et influença Empédocle<ref>Aristote, ''Métaphysique'', A, 3, 984a11-13 (DK 59 A 43) ; Diogène Laërce, VIII, 56 ; Denis O'Brien, « The Relation of Anaxagoras and Empedocles », ''Journal of Hellenic Studies'', vol. 88, 1968, p. 93-113, ici p. 93-94.</ref>.
Quelle que soit la date de son arrivée, Anaxagore est présenté par la tradition comme le premier philosophe à s'être établi à Athènes et à y avoir apporté la recherche ionienne sur la nature<ref>Clément d'Alexandrie, ''Stromates'', I, 63 (DK 59 A 7) : Anaxagore aurait transporté d'Ionie à Athènes l'enseignement d'Anaximène. La filiation avec Anaximène est chronologiquement peu vraisemblable (Curd, 2007, p. 129, n. 1).</ref>. Athènes, après ses victoires sur les Perses à Marathon (490) et à Salamine (480), devient alors le principal centre politique et intellectuel du monde grec. C'est dans ce contexte que la tradition situe l'enseignement d'Anaxagore.
La tradition biographique, dominée par le récit de Plutarque, lie Anaxagore à Périclès<ref>Plutarque, ''Vie de Périclès'', 4-6, 16, 32 (DK 59 A 15-17) ; Platon, ''Phèdre'', 269e-270a (DK 59 A 15) ; Isocrate, ''Sur l'échange'', 235 (DK 59 A 15), qui fait de Périclès le disciple d'Anaxagore et de Damon ; Diodore de Sicile, XII, 39 (DK 59 A 17), qui appelle Anaxagore « le maître de Périclès ».</ref>. Curd juge cette amitié « bien établie », mais Schofield invite à ne pas prendre trop au sérieux le titre d'« élève » donné à Périclès : il en retient seulement la vraisemblance d'une influence exercée dans la jeunesse de l'homme d'État<ref>Curd, 2007, p. 132 ; Schofield, 1980, p. 34. Sider (1981, p. 3) rappelle que ce genre de récit reflète souvent la seule conviction qu'un homme plus jeune a appris de son aîné.</ref>. L'image d'un philosophe naturaliste qui aurait donné au grand orateur démocratique son éloquence élevée provient d'abord du ''Phèdre'' de Platon ; elle appartient en partie à la construction biographique et ne doit pas être surinterprétée.
Parmi les auditeurs d'Anaxagore, la tradition mentionne le poète Euripide<ref>Diogène Laërce, II, 10 (DK 59 A 1) ; Alexandre d'Étolie, cité par Aulu-Gelle, ''Nuits attiques'', XV, 20 (DK 59 A 21). Curd (2007, p. 132-133, n. 13) souligne que la présence d'échos anaxagoréens chez Euripide n'implique pas une relation formelle de maître à élève.</ref> et Archélaos, qui aurait ensuite été le maître de Socrate<ref>Diogène Laërce, II, 16 (DK 60 A 1) ; Curd, 2007, p. 134 et n. 18.</ref>. La question de savoir si Socrate a rencontré Anaxagore reste ouverte. Dans le ''Phédon'', Socrate dit avoir entendu quelqu'un lire le livre d'Anaxagore, puis l'avoir lu lui-même<ref>Platon, ''Phédon'', 97b-99d (DK 59 A 47). Platon ne nomme pas le lecteur ; l'hypothèse selon laquelle il s'agirait d'Archélaos est une conjecture moderne (Curd, 2007, p. 134 ; Burnet la formule avec un point d'interrogation).</ref>. Certains en ont conclu qu'Anaxagore avait quitté Athènes avant que Socrate ne s'intéresse à la philosophie ; Curd juge cet argument du silence peu probant<ref>Curd, 2007, p. 134-136 ; en sens contraire, Woodbury, 1981, p. 297, et Schofield, 1980, p. 34-35.</ref>. Dans l’''Apologie'', Socrate rappelle que les livres d'Anaxagore pouvaient s'acquérir pour une drachme au plus « à l'orchestra »<ref>Platon, ''Apologie de Socrate'', 26d-e (DK 59 A 35). Sur l'interprétation de ce passage (achat de livres ou de lectures), voir Curd, 2007, p. 92, n. 12.</ref>, ce qui atteste la diffusion de ses idées à Athènes à la fin du V{{e}} siècle.
=== L'œuvre écrite ===
Anaxagore est l'auteur d'un seul ouvrage en prose ionienne, que Simplicius désigne comme un traité ''Sur la nature'' (Περὶ φύσεως) et dont il cite le « premier livre »<ref>Diogène Laërce, I, 16 (DK 59 A 37), range Anaxagore parmi les auteurs d'un seul livre ; Simplicius, ''Commentaire sur la Physique d'Aristote'', 34, 29 et 155, 26.</ref>. Platon emploie le pluriel τὰ Ἀναξαγόρου βιβλία dans l’''Apologie''<ref>Platon, ''Apologie'', 26d.</ref> : Burnet y voyait l'indice d'un ouvrage occupant plusieurs rouleaux, ce que Raven contestait<ref>John Burnet, ''Early Greek Philosophy'', 4{{e}} éd., Londres, A. & C. Black, 1930, chap. VI ; Sven-Tage Teodorsson, ''Anaxagoras' Theory of Matter'', Göteborg, Acta Universitatis Gothoburgensis, 1982, p. 10.</ref>. Les écrits qu'on lui attribue par ailleurs (sur la quadrature du cercle, sur la perspective, un recueil de problèmes) sont presque certainement apocryphes<ref>DK 59 A 38-40 ; Teodorsson, 1982, p. 10 ; Burnet, 1930, chap. VI.</ref>. Diogène Laërce rapporte encore qu'Anaxagore fut le premier à publier un livre accompagné de figures<ref>Diogène Laërce, II, 11 (DK 59 A 1).</ref>.
Diogène Laërce qualifie son style d'agréable et d'élevé<ref>Diogène Laërce, II, 6 (DK 59 A 1).</ref>. Schofield a analysé la texture de cette prose archaïque : syntaxe paratactique, répétitions, et, dans le fragment B12, un style de « prédication solennelle » qui accumule les attributs du Noûs à la manière d'un hymne<ref>Schofield, 1980, p. 6-9, qui s'appuie sur Karl Deichgräber, « Hymnische Elemente in der philosophischen Prosa der Vorsokratiker », ''Philologus'', vol. 88, 1933, p. 347-361, et renvoie à Eduard Norden et Hermann Fränkel.</ref>. Contrairement à Parménide et à Empédocle, qui écrivent en hexamètres, Anaxagore s'inscrit dans la tradition ionienne de la prose, inaugurée par Anaximandre et Anaximène. Il subsiste de ce traité une vingtaine de fragments, conservés pour la plupart par Simplicius de Cilicie (VI{{e}} siècle ap. J.-C.) dans ses commentaires sur Aristote<ref>Édition de référence : Hermann Diels et Walther Kranz, ''Die Fragmente der Vorsokratiker'', 6{{e}} éd., Berlin, Weidmann, 1951-1952, vol. II, p. 5-44 (chapitre 59, témoignages A et fragments B). Édition plus récente, avec une nouvelle numérotation (Anaxagore y porte le numéro 25) : André Laks et Glenn W. Most, ''Early Greek Philosophy'', 9 vol., Cambridge (Mass.), Harvard University Press, 2016 ; version française : ''Les Débuts de la philosophie'', Paris, Fayard, 2016. Voir aussi Curd, 2007, et Sider, 1981 (2{{e}} éd. 2005). Le fragment B20 est tenu pour inauthentique depuis Sider (Claire Louguet, compte rendu de Curd, 2007, ''Classical Review'', n.s. vol. 59, 2009, p. 23-24).</ref>. Les citations de Simplicius semblent limitées au premier livre, consacré aux principes généraux<ref>Burnet, 1930, chap. VI ; Teodorsson, 1982, p. 10. Schofield (1980, p. 159, n. 36) doute que Simplicius ait disposé du livre entier plutôt que d'extraits (cf. Curd, 2007, p. 165, n. 25).</ref>.
=== Le procès et l'exil ===
Les circonstances du départ d'Anaxagore d'Athènes sont particulièrement controversées. Diogène Laërce rapporte plusieurs versions de son procès<ref>Diogène Laërce, II, 12-14 (DK 59 A 1).</ref>. Selon Sotion, Anaxagore fut accusé d'impiété (ἀσέβεια) par Cléon pour avoir soutenu que le soleil était une masse de métal incandescent ; défendu par Périclès, il fut condamné à une amende de cinq talents et à l'exil<ref>Sotion, dans Diogène Laërce, II, 12.</ref>. Selon Satyros, l'accusateur fut Thucydide, fils de Mélésias, adversaire politique de Périclès, et l'accusation porta sur l'impiété et le médisme ; Anaxagore aurait été condamné à mort par contumace<ref>Satyros, dans Diogène Laërce, II, 12.</ref>. Hermippe le dit emprisonné en attente d'exécution puis libéré à la demande de Périclès ; Hiéronymos rapporte que Périclès le présenta au tribunal affaibli par la maladie, si bien qu'il fut acquitté par pitié<ref>Hermippe et Hiéronymos, dans Diogène Laërce, II, 13-14.</ref>.
Plutarque rapporte qu'un certain Diopeithès fit voter un décret autorisant les poursuites contre ceux qui ne reconnaissaient pas les dieux ou enseignaient des doctrines sur les phénomènes célestes, afin de jeter le soupçon sur Périclès à travers Anaxagore ; Périclès, craignant pour Anaxagore, l'éloigna de la cité<ref>Plutarque, ''Vie de Périclès'', 32, 1-5 (DK 59 A 17). Plutarque ne précise pas le contenu des doctrines incriminées. La thèse du soleil-pierre et de la lune-terre est mentionnée par Platon, ''Apologie'', 26d (DK 59 A 35), où Mélétos attribue ces opinions à Socrate. Plutarque (''Vie de Nicias'', 23 = DK 59 A 18) dit aussi que Périclès eut peine à tirer Anaxagore de prison.</ref>. Johannes Geffcken jugeait le décret de Diopeithès historiquement fondé et estimait que l'accusation visait l'ensemble de la « météorologie » d'Anaxagore, notamment son explication naturelle de la foudre, plus que la seule thèse du soleil<ref>Johannes Geffcken, « Die Asebeia des Anaxagoras », ''Hermes'', vol. 42, 1907, p. 127-133, ici p. 133.</ref>.
L'historicité du procès a été mise en doute, notamment par K. J. Dover, pour qui aucune des sources anciennes ne savait réellement ce qui était arrivé à Anaxagore ; J. A. Davison a au contraire reconstitué une vie comportant deux procès, l'un vers 456/5, l'autre vers 433-430, ce qui l'oblige à supposer une amnistie dont rien ne témoigne<ref>K. J. Dover, « The Freedom of the Intellectual in Greek Society », ''Talanta'', vol. 7, 1975, p. 24-54 (surtout p. 27-32), cité par Schofield, 1980, p. 34 et n. 74 ; J. A. Davison, « Protagoras, Democritus, and Anaxagoras », ''Classical Quarterly'', n.s. vol. 3, 1953, p. 33-45 (surtout p. 39-45), suivi par Guthrie et Russell Meiggs ; critique de cette reconstruction chez Woodbury, 1981, p. 303 (en note). Gershenson et Greenberg, dans ''Anaxagoras and the Birth of Physics'' (1964), nient également le procès (Teodorsson, 1982, p. 7, n. 2).</ref>. Curd estime néanmoins qu'il y a peu de raisons de douter de la tradition sur ce point<ref>Curd, 2007, p. 136.</ref>. La date reste incertaine : vers 450 selon A. E. Taylor et Burnet, au milieu du siècle selon Woodbury, qui rattache l'accusation portée par Thucydide, fils de Mélésias, aux luttes politiques de cette période, en 437/6 selon Mansfeld, vers 434 selon Sider, vers 430 selon Richard Janko<ref>A. E. Taylor, « On the Date of the Trial of Anaxagoras », ''Classical Quarterly'', vol. 11, 1917, p. 81-87 ; Burnet, 1930, chap. VI ; Woodbury, 1981, p. 315 ; Mansfeld, 1979, p. 39 ; Sider, 1981, p. 1-11 ; Richard Janko, « Eclipse and Plague: Themistocles, Pericles, Anaxagoras and the Athenians' War on Science », ''Journal of Hellenic Studies'', vol. 140, 2020, p. 213-237. Pour la synthèse, voir Curd et Sisko, ''SEP'', 2026, § 1.</ref>.
Anaxagore se retira ensuite à Lampsaque, sur l'Hellespont (l'actuel détroit des Dardanelles), où il fut honoré<ref>Diogène Laërce, II, 14-15 (DK 59 A 1) ; Alcidamas, cité par Aristote, ''Rhétorique'', II, 23, 1398b15-16 (DK 59 A 23).</ref>. Il y mourut en 428 av. J.-C., à soixante-douze ans selon Apollodore<ref>Diogène Laërce, II, 7 (DK 59 A 1).</ref>. Invité par les magistrats de la cité à exprimer un vœu, il aurait demandé que les enfants aient congé chaque année dans le mois de sa mort, usage encore observé au temps de Diogène Laërce<ref>Diogène Laërce, II, 14 (DK 59 A 1).</ref>. Élien rapporte qu'un autel lui fut élevé, portant les inscriptions « Intellect » (Νοῦς) et « Vérité » (Ἀλήθεια)<ref>Élien, ''Histoire variée'', VIII, 19 (DK 59 A 24). Le texte peut signifier soit un autel portant une inscription sur chaque face, soit un autel dédié aux deux (Curd, 2007, p. 88, n. 10).</ref>. L'épitaphe gravée sur sa tombe nous est parvenue : « Ci-gît Anaxagore, qui atteignit le plus lointain terme de la vérité sur le cosmos céleste »<ref>Diogène Laërce, II, 15 (DK 59 A 1) ; Élien, ''Histoire variée'', VIII, 19 (DK 59 A 24). Alcidamas, au IV{{e}} siècle, atteste que les Lampsacéniens l'honoraient encore (DK 59 A 23) ; Gershenson et Greenberg (1964, p. 4) rappellent que sa mémoire fut honorée pendant plus d'un siècle.</ref>. Cicéron rapporte enfin qu'à des amis qui lui demandaient, à Lampsaque, s'il voulait être ramené à Clazomènes, il aurait répondu que le chemin des Enfers est le même de partout<ref>Cicéron, ''Tusculanes'', I, 43, 104 (DK 59 A 34a) ; cf. Diogène Laërce, II, 11.</ref>.
=== Portrait et anecdotes ===
Les sources anciennes conservent de nombreuses anecdotes qui, sans garantie historique, témoignent de l'image du philosophe dans la tradition. Apprenant la mort de son fils, Anaxagore aurait dit avec le plus grand calme : « Je savais que j'avais engendré un mortel »<ref>Galien, ''Des opinions d'Hippocrate et de Platon'', IV, 7 (DK 59 A 33) ; Diogène Laërce, II, 13, rapporte le mot au pluriel, à propos de ses enfants.</ref>. Ces récits ont fait de lui, dans la tradition, le modèle du sage détaché des contingences et consacré à la contemplation de la nature.
Dans le ''Phèdre'', Socrate attribue l'élévation de l'éloquence de Périclès à sa fréquentation d'Anaxagore, dont les discours portaient pour l'essentiel sur la nature de l'intelligence et de la déraison (νοῦ τε καὶ ἀνοίας)<ref>Platon, ''Phèdre'', 269e-270a (DK 59 A 15) ; Schofield, 1980, p. 22-23.</ref>. Cette remarque témoigne de la réputation d'Anaxagore dans l'Athènes classique : celle d'un penseur dont les spéculations « météorologiques » (au sens ancien de l'étude des phénomènes célestes) pouvaient paraître étranges au commun. Les ''Nuées'' d'Aristophane tournent en ridicule la nouvelle science de la nature ; toutefois, selon Dover, que suit Schofield, les idées moquées dans la pièce se rattachent plus aisément à Diogène d'Apollonie qu'à Anaxagore<ref>Aristophane, ''Les Nuées'' ; Kenneth Dover, ''Aristophanes: Clouds'', Oxford, Clarendon Press, 1968, introduction ; Schofield, 1980, p. 35 et n. 83 ; Curd, 2007, p. 132, n. 11. Gábor Betegh a proposé de voir aussi dans la pièce des échos d'Archélaos (Betegh, 2016).</ref>.
=== L'école de Lampsaque ===
Anaxagore semble avoir poursuivi son activité à Lampsaque et y avoir fondé une école prospère<ref>Schofield, 1980, p. 35 et n. 84, qui s'appuie sur Diogène Laërce, II, 14-15, Aristote, ''Rhétorique'', 1398b15 (DK 59 A 23), et Eusèbe, ''Préparation évangélique'', X, 14, 13 (DK 59 A 7) ; Sider, 1981, p. 3-4 ; Burnet, 1930, chap. VI. Curd (2007, p. 129, n. 1) rappelle que nous ignorons la nature exacte de son activité philosophique, et notamment s'il eut une école.</ref>. Eusèbe rapporte qu'Archélaos lui succéda à la tête de cette école de Lampsaque<ref>Eusèbe, ''Préparation évangélique'', X, 14, 13 (DK 59 A 7). Cette indication s'accorde mal avec les témoignages qui situent Archélaos à Athènes ; Curd (2007, p. 134, n. 18) note que la tradition a pu confondre certains aspects de la vie des deux hommes.</ref>. Parmi ses proches, les sources mentionnent aussi Métrodore de Lampsaque, qui prolongea l'interprétation d'Homère attribuée à Anaxagore en étudiant la « physique » du poète<ref>Diogène Laërce, II, 11 (DK 59 A 1), d'après Favorinus ; DK 61. Schofield (1980, p. 149, n. 59) range Archélaos (DK 60 A 1-5) et Métrodore (DK 61 A 2, 6) parmi ses disciples.</ref>. Cette présence à Lampsaque contribue sans doute à expliquer que ses idées aient continué de circuler dans le monde grec après son départ d'Athènes.
== Les principes métaphysiques ==
La philosophie d'Anaxagore se construit pour une large part en réponse aux exigences posées par Parménide d'Élée<ref>Curd, 2007, p. 137-142 ; Patricia Curd, ''The Legacy of Parmenides: Eleatic Monism and Later Presocratic Thought'', Princeton, Princeton University Press, 1998 (rééd. Las Vegas, Parmenides Publishing, 2004) ; Schofield, 1980, p. 5.</ref>. Dans son poème, Parménide oppose deux voies de recherche : celle « qu'il est et qu'il n'est pas possible qu'il ne soit pas » (ὅπως ἔστιν τε καὶ ὡς οὐκ ἔστι μὴ εἶναι) et celle « qu'il n'est pas et qu'il faut qu'il ne soit pas »<ref>Parménide, DK 28 B 2, 3-5 ; cf. B 6, 1-2 (ἔστι γὰρ εἶναι, μηδὲν δ' οὐκ ἔστιν, « car être est, et le néant n'est pas »).</ref>. Il en conclut que toute génération et toute corruption véritables sont impossibles, car elles supposeraient un passage de ce qui n'est pas à ce qui est, ou l'inverse<ref>Parménide, B 8, 6-21 ; cf. Aristote, ''Physique'', I, 8, 191a23-31.</ref>. Ce qui est véritablement doit en outre être un, continu, homogène et immobile<ref>Parménide, B 8, 22 : « Il n'est pas divisible, puisqu'il est tout entier semblable » (οὐδὲ διαιρετόν ἐστιν, ἐπεὶ πᾶν ἐστιν ὁμοῖον).</ref>.
Anaxagore accepte l'impossibilité de la génération à partir de ce qui n'est pas, mais refuse d'en conclure que la pluralité et le changement sont illusoires<ref>Aristote, ''Métaphysique'', A, 3, 984a11-16 (DK 59 A 43) ; ''Physique'', I, 4, 187a26-29 (DK 59 A 52).</ref>. Théophraste, cité par Simplicius, dit qu'Anaxagore, qui avait partagé la philosophie d'Anaximène, fut le premier à modifier les doctrines sur les principes et à fournir la cause qui leur manquait<ref>Théophraste, dans Simplicius, ''Commentaire sur la Physique'', 27, 2-4 (DK 59 A 41). La « cause qui manquait » désigne vraisemblablement le Noûs, cause du mouvement (Curd, 2007, p. 93, n. 13). Le lien avec Anaximène doit s'entendre au sens large d'une appartenance à la tradition ionienne (Curd, 2007, p. 129, n. 1 ; Burnet, 1930, chap. VI).</ref>. Plutôt que de nier le devenir, Anaxagore cherche à fonder une cosmologie qui satisfasse les exigences éléates tout en rendant compte de la multiplicité et du mouvement observables. Schofield souligne toutefois qu'il reste un cosmologue ionien, qui intègre certaines thèses de Parménide sans adopter sa méthode argumentative<ref>Schofield, 1980, p. 5 et 26-28 ; G. E. L. Owen, « Eleatic Questions », ''Classical Quarterly'', n.s. vol. 10, 1960, p. 84-102. Sur le débat entre ceux qui voient en Anaxagore un critique de Parménide et ceux qui en font un disciple, voir d'un côté Daniel W. Graham, « Empedocles and Anaxagoras: Responses to Parmenides », dans A. A. Long (éd.), ''The Cambridge Companion to Early Greek Philosophy'', Cambridge, Cambridge University Press, 1999, p. 159-180, et de l'autre John E. Sisko, « Anaxagoras' Parmenidean Cosmology: Worlds within Worlds within the One », ''Apeiron'', vol. 36, 2003, p. 87-114 ; voir aussi Curd et Sisko, ''SEP'', 2026, § 2.</ref>. G. E. L. Owen a relevé que la première phrase du traité, « Toutes choses étaient ensemble », est « manifestement formulée comme une contradiction plate de Parménide » sur plusieurs points essentiels<ref>G. E. L. Owen, « Plato and Parmenides on the Timeless Present », ''The Monist'', vol. 50, 1966, p. 317-340, repris dans A. P. D. Mourelatos (éd.), ''The Pre-Socratics'', Garden City, Anchor Press, 1974, p. 271-292, ici p. 276-277 ; cité par Schofield, 1980, p. 64 et n. 51.</ref>.
=== Le principe de conservation ===
Le premier principe d'Anaxagore est énoncé dans le fragment B17 : « Les Grecs ne pensent pas correctement la naissance et la destruction : aucune chose ne naît ni ne périt, mais, à partir des choses qui sont, il y a mélange et dissociation. Ainsi auraient-ils raison d'appeler la naissance “mélange” et la destruction “dissociation” » (τὸ γίνεσθαι συμμίσγεσθαι καὶ τὸ ἀπόλλυσθαι διακρίνεσθαι)<ref>Anaxagore, fragment B17, cité par Simplicius, ''Commentaire sur la Physique'', 163, 18-24 (DK 59 B 17 ; LM 25 D15). Jochen Althoff compare ce fragment en prose au fragment 31 B8 d'Empédocle, qui exprime en vers une idée voisine (« Presocratic Discourse in Poetry and Prose: The Case of Empedocles and Anaxagoras », ''Studies in History and Philosophy of Science'', vol. 43, 2012, p. 293-299).</ref>. Ce principe constitue la base de sa physique et sa réponse à l'interdit parménidien. Ce qui apparaît comme génération n'est qu'un réarrangement d'ingrédients préexistants qui se mélangent (συμμίσγεται) ; ce qui paraît destruction n'est que leur dissociation (διακρίνεται)<ref>Aristote, ''Métaphysique'', A, 3, 984a13-16 (DK 59 A 43) : selon Anaxagore, presque tous les homéomères naissent et périssent seulement par agrégation et dissociation. Curd (2007, p. 145) rappelle que l'assimilation de la génération à l'altération, qu'Aristote prête à Anaxagore (''Génération et corruption'', I, 1, 314a11-13 = DK 59 A 52), est une interprétation aristotélicienne.</ref>. Rien ne naît du néant, rien n'y retourne : les ingrédients de la réalité existent de toute éternité et conservent leur nature propre.
Aristote explique la position d'Anaxagore par son adhésion à l'opinion commune des physiciens selon laquelle rien ne naît de ce qui n'est pas<ref>Aristote, ''Physique'', I, 4, 187a26-29 (DK 59 A 52) ; Schofield, 1980, p. 43-44.</ref>. Le principe prend une forme plus précise dans une question que la tradition attribue à Anaxagore : « Comment le cheveu pourrait-il naître de ce qui n'est pas cheveu, et la chair de ce qui n'est pas chair ? » (πῶς γὰρ ἂν ἐκ μὴ τριχὸς γένοιτο θρὶξ καὶ σὰρξ ἐκ μὴ σαρκός;)<ref>Anaxagore, fragment B10, transmis par une scholie à Grégoire de Nazianze (DK 59 B 10). L'authenticité de la formule est discutée : Schofield l'a d'abord contestée (« Doxographica Anaxagorea », ''Hermes'', vol. 103, 1975, p. 1-24), puis a admis que le scholiaste, dont la source pourrait être Eudème, en conserve peut-être les termes (Schofield, 1980, p. 133-143). La plupart des spécialistes la tiennent pour authentique (Sider ; Curd, 2007, p. 53-54).</ref>. Cette formule exprime ce que les commentateurs appellent le principe « du semblable par le semblable » : une substance ne peut provenir que de la même substance, déjà présente sous une forme non manifeste<ref>Schofield, 1980, p. 44 et 55-58, qui analyse ce principe dans la reconstruction aristotélicienne.</ref>.
On rapproche parfois ce principe de la loi de conservation de la matière formulée par Antoine-Laurent de Lavoisier et de la maxime « Rien ne se perd, rien ne se crée, tout se transforme ». L'analogie ne doit pas être surinterprétée : les mécanismes qu'Anaxagore propose pour expliquer les transformations (mélange et dissociation d'ingrédients éternels) sont qualitatifs et ne relèvent pas d'une chimie quantitative au sens moderne<ref>Antoine-Laurent de Lavoisier, ''Traité élémentaire de chimie'', Paris, Cuchet, 1789, t. I, chap. XIII, où est posé le principe d'une égale quantité de matière avant et après toute opération. Le rapprochement avec Anaxagore relève de l'analogie rétrospective, non d'une filiation historique.</ref>. L'intuition d'Anaxagore partage avec le principe de Lavoisier l'idée que rien ne se perd ni ne se crée dans le devenir physique ; il serait anachronique d'y voir davantage.
=== Tout est dans tout ===
Le deuxième principe s'énonce ainsi : « En toute chose il y a une part de toute chose, sauf de l'Intellect ; et il est des choses dans lesquelles l'Intellect aussi est présent » (ἐν παντὶ παντὸς μοῖρα ἔνεστι πλὴν νοῦ, ἔστιν οἷσι δὲ καὶ νοῦς ἔνι)<ref>Anaxagore, fragment B11, cité par Simplicius, ''Commentaire sur la Physique'', 164, 23-24 (DK 59 B 11). La même thèse figure en B6 et au début de B12.</ref>. Ce principe, souvent désigné par la formule latine ''omnia in omnibus'', est la thèse la plus caractéristique et la plus déroutante de la philosophie d'Anaxagore ; Curd le nomme « principe du mélange universel »<ref>Curd, 2007, p. 179 et n. 3 ; cf. Gregory Vlastos, « The Physical Theory of Anaxagoras », ''Philosophical Review'', vol. 59, 1950, p. 31-57 ; Colin Strang, « The Physical Theory of Anaxagoras », ''Archiv für Geschichte der Philosophie'', vol. 45, 1963, p. 101-118.</ref>. Il signifie que toute portion de matière, si petite soit-elle, contient des parts de tous les ingrédients qui existent : aucun ingrédient n'existe à l'état pur, isolé de tous les autres<ref>Anaxagore, B6 : « Puisqu'il n'est pas possible qu'il y ait un plus petit, rien ne pourrait être séparé ni venir à être par soi-même, mais, comme au commencement, maintenant aussi toutes choses sont ensemble » (ὅτε τοὐλάχιστον μὴ ἔστιν εἶναι, οὐκ ἂν δύναιτο χωρισθῆναι, οὐδ' ἂν ἐφ' ἑαυτοῦ γενέσθαι, ἀλλ' ὅπωσπερ ἀρχὴν εἶναι καὶ νῦν πάντα ὁμοῦ).</ref>. Ce qui nous apparaît comme de l'or contient, outre l'or qui y prédomine, des parts de tous les autres ingrédients<ref>Théophraste, dans Simplicius, ''Commentaire sur la Physique'', 27, 2-11 (DK 59 A 41) ; Aristote, ''Physique'', I, 4, 187a36-b7.</ref>.
La tradition doxographique rattache cette thèse aux phénomènes de la nutrition et de la croissance. Comment la chair pourrait-elle provenir du pain et de l'eau que nous consommons, si le pain et l'eau ne contenaient pas déjà de la chair ? La nourriture doit donc contenir, de manière imperceptible, toutes les substances qui composent le corps<ref>Aétius, I, 3, 5 (DK 59 A 46) ; Simplicius, ''Commentaire sur la Physique'', 460, 4-20 (DK 59 A 45). La scholie qui transmet B10 affirme que la semence contient cheveux, ongles, veines, artères, nerfs et os, imperceptibles en raison de leur petitesse et qui se séparent peu à peu au cours de la croissance.</ref>. Curd et Schofield soulignent cependant que la nutrition n'est qu'un cas, particulièrement frappant, d'une question métaphysique plus générale sur le devenir, héritée de Parménide<ref>Curd, 2007, p. 179-180 ; Schofield, 1980, p. 121 et 133-143.</ref>.
Le principe vaut aussi pour les qualités opposées. Selon la scholie à Grégoire de Nazianze, Anaxagore affirmait qu'il y a du noir dans le blanc et du blanc dans le noir, et du léger dans le lourd<ref>Scholie à Grégoire de Nazianze, contexte de DK 59 B 10 (texte et traduction dans Schofield, 1980, p. 135-136). Sextus Empiricus (''Hypotyposes pyrrhoniennes'', I, 33 = DK 59 A 97) rapporte qu'Anaxagore disait la neige noire, puisqu'elle est de l'eau congelée et que l'eau est noire.</ref>. Ces oppositions ne sont donc pas absolues : ce qui nous paraît blanc contient du noir, en proportion trop faible pour être perçue.
Cette doctrine suscite des débats depuis l'Antiquité. Aristote y voit la conséquence de l'observation selon laquelle n'importe quoi naît de n'importe quoi<ref>Aristote, ''Physique'', III, 4, 203a23-33 (DK 59 A 45) ; Schofield, 1980, p. 43-52.</ref>. Les interprètes modernes se divisent sur la nature des « parts » (μοῖραι). Pour les uns, il s'agit de particules infiniment petites (lecture déjà présente chez Lucrèce) ; pour les autres, de proportions ou de concentrations variables d'ingrédients qui se compénètrent sans structure corpusculaire<ref>Lecture particulaire : Lucrèce, ''De la nature'', I, 830-920 (DK 59 A 44) ; Vlastos, 1950 ; W. K. C. Guthrie, ''A History of Greek Philosophy'', vol. II, Cambridge, Cambridge University Press, 1965, p. 289 ; George B. Kerferd, « Anaxagoras and the Concept of Matter before Aristotle », ''Bulletin of the John Rylands Library'', vol. 52, 1969, p. 129-143 ; Sider, 1981. Lecture non particulaire : J. E. Raven, « The Basis of Anaxagoras' Cosmology », ''Classical Quarterly'', n.s. vol. 4, 1954, p. 123-137, ici p. 128-130 ; Schofield, 1980, p. 73-79 ; Jonathan Barnes, ''The Presocratic Philosophers'', Londres, Routledge, 1982, p. 323-326 ; Brad Inwood, « Anaxagoras and Infinite Divisibility », ''Illinois Classical Studies'', vol. 11, 1986, p. 17-33, ici p. 17-18 ; Curd, 2007, p. 183-184 ; Anna Marmodoro, « Anaxagoras's Qualitative Gunk », ''British Journal for the History of Philosophy'', vol. 23, 2015, p. 402-422, et ''Everything in Everything: Anaxagoras's Metaphysics'', New York, Oxford University Press, 2017, chap. 3-4. Voir le recensement de Curd, 2007, p. 183, n. 9-10. Pour d'autres lectures du principe : Margaret E. Reesor, « The Meaning of Anaxagoras », ''Classical Philology'', vol. 55, 1960, p. 1-8 ; Thomas D. Paxson Jr., « The Holism of Anaxagoras », ''Apeiron'', vol. 17, 1983, p. 85-91, pour qui seul le plénum existe par soi ; Adam Drozdek, « Anaxagoras and the Everything in Everything Principle », ''Hermes'', vol. 133, 2005, p. 163-177.</ref>. Quoi qu'il en soit, ce principe forme la clé de voûte de sa physique.
=== Pas de plus petit ni de plus grand ===
Le troisième principe est exposé dans le fragment B3 : « Car du petit il n'y a pas de plus petit, mais toujours un plus petit (car ce qui est ne peut pas ne pas être) ; mais du grand aussi il y a toujours un plus grand, et il est égal au petit en quantité ; et, rapportée à elle-même, chaque chose est à la fois grande et petite » (οὔτε γὰρ τοῦ σμικροῦ ἐστί τό γε ἐλάχιστον, ἀλλ' ἔλασσον ἀεί· τὸ γὰρ ἐὸν οὐκ ἔστι τὸ μὴ οὐκ εἶναι)<ref>Anaxagore, fragment B3, cité par Simplicius, ''Commentaire sur la Physique'', 164, 17-20 (DK 59 B 3). Zeller a proposé de corriger τὸ μή des manuscrits en τομῇ (« par division ») : la parenthèse signifierait alors que ce qui est ne peut cesser d'être par division. Burnet et Sider adoptent la correction ; Curd défend le texte des manuscrits (Curd, 2007, p. 39-40 ; Schofield, 1980, p. 156-157, n. 15).</ref>. Ce principe exclut l'existence d'un plus petit et d'un plus grand, et s'oppose à toute conception atomiste<ref>Leucippe et Démocrite, DK 67-68 ; Aristote, ''Génération et corruption'', I, 2, 315b28-317a2, et I, 8, 325a23-b5.</ref>.
Contrairement à Leucippe et à Démocrite, qui posent des corps insécables (ἄτομα), Anaxagore soutient qu'aucune portion de matière n'est minimale et que toute portion, si petite soit-elle, contient encore toutes choses<ref>Anaxagore, B6 : « Puisque les parts du grand et du petit sont égales en quantité, de cette manière aussi toutes choses seraient en tout » (καὶ ὅτε δὲ ἴσαι μοῖραί εἰσι τοῦ τε μεγάλου καὶ τοῦ σμικροῦ πλῆθος, καὶ οὕτως ἂν εἴη ἐν παντὶ πάντα).</ref>. Cette infinité dans la petitesse se double d'une infinité dans la grandeur : il n'existe pas plus de limite supérieure que de limite inférieure. Cette double infinité pose des problèmes interprétatifs importants, tant aux commentateurs anciens qu'aux modernes<ref>Montgomery Furth, « A “Philosophical Hero”? Anaxagoras and the Eleatics », ''Oxford Studies in Ancient Philosophy'', vol. 9, 1991, p. 95-129 ; David J. Furley, « Anaxagoras, Plato and the Naming of Parts », dans Victor Caston et Daniel W. Graham (éd.), ''Presocratic Philosophy: Essays in Honour of Alexander Mourelatos'', Aldershot, Ashgate, 2002, p. 119-126 ; Marmodoro, 2017, chap. 2-3.</ref>. Miloš Arsenijević, Saša Popović et Miloš Vuletić voient en lui un infinitiste conséquent, qui étend à la physique le principe d'isotropie de l'espace géométrique, si bien que deux parties quelconques du mélange originel sont semblables entre elles<ref>Miloš Arsenijević, Saša Popović et Miloš Vuletić, « Anaxagoras, the Thoroughgoing Infinitist: The Relation between his Teachings on Multitude and on Heterogeneity », ''European Journal of Analytic Philosophy'', vol. 15, 2019, p. 35-70, ici p. 35.</ref>.
Le principe de non-minimum a une conséquence importante : le mélange universel ne pourra jamais être défait. Puisqu'il n'existe pas de plus petite quantité d'un ingrédient, celui-ci ne peut jamais être entièrement extrait d'un mélange ; sa proportion peut diminuer indéfiniment sans jamais devenir nulle<ref>Anaxagore, B6 ; Curd, 2007, p. 182-185. Simplicius commente B3 en ces termes : si tout est en tout et si tout se sépare de tout, alors, de ce qui semble le plus petit, se séparera encore quelque chose de plus petit, et ce qui semble le plus grand s'est séparé de quelque chose de plus grand (''Commentaire sur la Physique'', 164, 20-23).</ref>. Comme le formule Malcolm Schofield à la suite de Colin Strang, la complexité de la composition n'est pas fonction de la taille<ref>Schofield, 1980, p. 69, 79 et 90 ; Strang, 1963, repris dans David J. Furley et R. E. Allen (éd.), ''Studies in Presocratic Philosophy'', vol. II, Londres, Routledge and Kegan Paul, 1975, p. 361-380, ici p. 366 ; cf. Curd, 2007, p. 40 et 50, n. 35.</ref>.
La justification donnée en B3 reprend l'axiome parménidien : « car ce qui est ne peut pas ne pas être ». Si l'on pouvait diviser un ingrédient jusqu'à le faire disparaître, il y aurait passage de l'être au non-être, ce qu'interdit Parménide. Toute division, aussi poussée soit-elle, laisse subsister quelque chose, et ce reste contient encore des parts de tous les ingrédients<ref>Curd, 2007, p. 39-40 et 184-185.</ref>.
=== Le principe de prédominance ===
Du principe « tout est dans tout » découle une difficulté : si chaque chose contient une part de toutes les autres, comment expliquer que nous percevions des objets distincts ? Anaxagore la résout par ce que les commentateurs modernes appellent le « principe de prédominance », expression qui n'est pas la sienne<ref>Schofield, 1980, p. 87 (où il cite les « principles of latency and predominance » de David Furley) et p. 108-111 ; Curd, 2007, p. 188-189 ; Marmodoro (2017, chap. 2) parle de ''preponderance principle''. Les commentateurs néoplatoniciens formulaient déjà les principes du « tout en tout » et de la prédominance (Schofield, 1980, p. 155, n. 52).</ref>. La fin du fragment B12 l'énonce ainsi : « Chaque chose une est et était, de la manière la plus manifeste, ce dont il y a le plus en elle » (ἀλλ' ὅτῳ πλεῖστα ἔνι, ταῦτα ἐνδηλότατα ἓν ἕκαστόν ἐστι καὶ ἦν)<ref>Anaxagore, fragment B12, fin, cité par Simplicius, ''Commentaire sur la Physique'', 156, 13-157, 4 (DK 59 B 12 ; LM 25 D27). La proposition qui précède (νοῦς δὲ πᾶς ὅμοιός ἐστι καὶ ὁ μείζων καὶ ὁ ἐλάττων· ἕτερον δὲ οὐδέν ἐστιν ὅμοιον οὐδενί) est diversement comprise ; A. Wasserstein propose de donner à ὅμοιος le même sens, « homogène », dans ses deux emplois, de sorte que la phrase opposerait l'homogénéité du Noûs au reste des choses (« A Note on Fragment 12 of Anaxagoras », ''Classical Review'', n.s. vol. 10, 1960, p. 4-5).</ref>.
Une chose tire donc son identité apparente des ingrédients qui y prédominent : un morceau d'or nous apparaît comme de l'or parce que l'or y est présent dans une proportion supérieure à celle des autres ingrédients<ref>Théophraste, dans Simplicius, ''Commentaire sur la Physique'', 27, 2-11 (DK 59 A 41) : chaque chose est caractérisée par ce qui prédomine en elle ; Aristote, ''Physique'', I, 4, 187b1-7.</ref>. Cette prédominance n'est jamais absolue, puisque tous les autres ingrédients demeurent présents, mais elle suffit à conférer à l'objet ses caractères perceptibles. Curd propose de la comprendre en termes de concentration ou de densité relative plutôt que de quantité brute<ref>Curd, 2007, p. 188-189.</ref>.
Le principe de prédominance permet de concilier l'ontologie du mélange universel avec l'expérience d'un monde d'objets distincts. Il rend compte aussi du changement : lorsque le pain devient chair, la chair déjà présente dans le pain vient s'ajouter à la chair du corps, tandis que les autres ingrédients se dispersent<ref>Aristote, ''Génération des animaux'', I, 18, 723a10-11 ; Simplicius, ''Commentaire sur la Physique'', 460, 4-20 (DK 59 A 45) ; Platon, ''Phédon'', 96c-d (DK 59 A 46).</ref>. Le changement apparent se ramène ainsi à une modification des proportions relatives des ingrédients.
Selon Théophraste, Anaxagore soutenait que la perception se fait par les contraires et que toute sensation s'accompagne de douleur, parce que le contact du dissemblable produit une gêne<ref>Théophraste, ''Du sens'', 27-29 (DK 59 A 92) ; Aétius, IV, 9, 16 (DK 59 A 94).</ref>. Les sens ne discernent que les différences marquées de proportion ; Anaxagore reconnaissait leur faiblesse, tout en affirmant que « les choses qui apparaissent sont une vue des choses invisibles » (ὄψις γὰρ τῶν ἀδήλων τὰ φαινόμενα)<ref>Anaxagore, fragments B21 (Sextus Empiricus, ''Contre les mathématiciens'', VII, 90 : « à cause de la faiblesse des sens, nous ne sommes pas capables de discerner le vrai ») et B21a (Sextus Empiricus, ''Contre les mathématiciens'', VII, 140) ; Schofield, 1980, p. 24-25.</ref>.
=== Synthèse : la réponse d'Anaxagore à Parménide ===
Les quatre principes examinés, à savoir la conservation, le « tout est dans tout », l'absence de plus petit et la prédominance, forment un ensemble cohérent qui constitue la réponse d'Anaxagore à l'exigence parménidienne<ref>Curd, 2007, p. 137-142 et 178-191 ; Schofield, 1980, p. 36-99.</ref>. Anaxagore accepte l'impossibilité du passage de l'être au non-être, mais rejette les conséquences que Parménide en tirait quant à l'unicité, à l'immobilité et à l'homogénéité de l'être.
En remplaçant la génération et la corruption par le mélange et la dissociation (B17), il rend compte des phénomènes sans enfreindre l'interdit éléate. En posant que tout est dans tout (B6, B11), il explique comment des substances en apparence nouvelles peuvent émerger sans naître de rien : elles étaient déjà présentes, mais imperceptibles. En affirmant qu'il n'y a pas de plus petit (B3, B6), il garantit que le mélange universel ne sera jamais défait. En introduisant le principe de prédominance (B12), il rend compte de la diversité des apparences et de la possibilité de la perception.
Anaxagore entend ainsi préserver la permanence de ce qui est et l'impossibilité du non-être tout en rendant compte de la pluralité et du devenir. Les spécialistes restent partagés sur le degré de son engagement éléate : Curd met l'accent sur la dimension parménidienne du système, Schofield sur sa fidélité au style dogmatique de la cosmologie ionienne<ref>Curd, 2007, p. 141-142 ; Schofield, 1980, p. 26-28 et 142-143 ; Daniel W. Graham, ''Explaining the Cosmos'', 2006.</ref>.
== Les ingrédients primordiaux ==
La question de savoir quels sont exactement les ingrédients (τὰ χρήματα) qui composent l'univers d'Anaxagore divise les commentateurs depuis Aristote<ref>Pour une vue d'ensemble : Schofield, 1980, p. 100-144 ; Curd, 2007, p. 147-150 et 153-191 ; Daniel W. Graham, « Was Anaxagoras a Reductionist? », ''Ancient Philosophy'', vol. 24, 2004, p. 1-18 ; Curd et Sisko, ''SEP'', 2026, § 3.2.</ref>. Anaxagore ne fournit pas de liste systématique, et les fragments conservés mentionnent des entités de natures apparemment différentes. Schofield observe que, malgré le « rôle cardinal » de la doctrine du « tout est dans tout », les fragments montrent qu'Anaxagore accordait au moins autant de place, dans son exposé, au mélange primordial et à l'action cosmogonique du Noûs<ref>Schofield, 1980, p. 100.</ref>.
=== Les opposés ===
Les fragments B4b, B8, B12 et B15 énumèrent plusieurs couples d'opposés : l'humide et le sec (τὸ διερόν καὶ τὸ ξηρόν), le chaud et le froid (τὸ θερμόν καὶ τὸ ψυχρόν), le brillant et l'obscur (τὸ λαμπρόν καὶ τὸ ζοφερόν), le dense et le rare (τὸ πυκνόν καὶ τὸ ἀραιόν)<ref>Anaxagore, B4b, B8, B12, B15. Les fragments B1 et B2 mentionnent l'air et l'éther, non des couples d'opposés.</ref>. Ces opposés jouent un rôle cosmologique central : c'est leur séparation progressive (ἀποκρίνεσθαι) à partir du mélange originel qui produit la diversité des phénomènes<ref>Anaxagore, B12, B13, B15, B16 ; Aristote, ''Physique'', I, 4, 187a25-26 et 187b1-7.</ref>.
Les opposés ne doivent pas être compris comme de simples attributs d'une matière sous-jacente. La distinction entre substance et qualité, et la notion même de « matière » (ὕλη), sont des élaborations aristotéliciennes qu'il est anachronique de projeter sur Anaxagore<ref>Curd, 2007, p. 141, n. 32. Burnet (1930, chap. VI) souligne que le chaud et le froid, le sec et l'humide sont pour Anaxagore des « choses » (χρήματα).</ref>. Le chaud n'est pas une propriété d'une matière indéterminée, mais une réalité présente en plus ou moins grande proportion dans un mélange. F. M. Cornford parle à leur propos de « choses-qualités » (''quality-things'')<ref>F. M. Cornford, « Anaxagoras' Theory of Matter », ''Classical Quarterly'', vol. 24, 1930, p. 14-30 et 83-95, ici p. 84 (cité par Teodorsson, 1982, p. 35).</ref> ; Gregory Vlastos les décrit comme des puissances dont la combinaison en certaines proportions produit les substances naturelles<ref>Vlastos, 1950, repris dans ''Studies in Greek Philosophy'', vol. I, éd. Daniel W. Graham, Princeton, Princeton University Press, 1995, p. 303-327, ici p. 322 (cité par Curd, 2007, p. 168, n. 31).</ref>.
=== L'interprétation « austère » : les opposés seuls ===
Paul Tannery fut l'un des premiers à contester l'interprétation aristotélicienne selon laquelle Anaxagore aurait posé des « homéomères » élémentaires (chair, os, etc.) et à faire des opposés les véritables constituants<ref>Paul Tannery, « La théorie de la matière d'Anaxagore », ''Revue philosophique'', vol. 22, 1886, p. 255-274 ; repris dans ''Pour l'histoire de la science hellène'', Paris, Alcan, 1887 (Burnet renvoie à la p. 283 sq.). Voir Teodorsson, 1982, p. 30.</ref>. John Burnet adopta une position voisine ; il remarquait que, même après la définition de la notion de qualité (ποιότης), cette manière de penser les opposés comme des choses avait survécu, et s'appuyait sur Galien, pour qui les qualités sont éternelles chez Anaxagore<ref>Burnet, 1930, chap. VI, § « The Portions », p. 263 et note, qui cite Galien, ''Des facultés naturelles'', I, 2.</ref>.
Cette interprétation présente plusieurs avantages. Elle s'appuie étroitement sur les fragments plutôt que sur les reconstructions d'Aristote, qui écrivait environ un siècle après Anaxagore avec ses propres préoccupations. Elle rend compte du rôle cosmologique des opposés dans les fragments B12, B15 et B16<ref>Schofield, 1980, p. 107-121 ; Curd, 2007, p. 156-157 et 163-166.</ref>. Elle a été défendue, sous des formes diverses, par Tannery, Burnet, Cornford, Vlastos, Schofield, Inwood, Sedley et Marmodoro<ref>Curd, 2007, p. 156, n. 8, et p. 164, n. 21 ; Marmodoro, 2017, chap. 1, § 1.1, n. 4 ; Curd et Sisko, ''SEP'', 2026, § 3.2 ; David Sedley, ''Creationism and Its Critics in Antiquity'', Berkeley, University of California Press, 2007. Schofield (1980, p. 114-116 et 132-133) distingue des ingrédients fondamentaux (les opposés) et des ingrédients dérivés (air, terre, eau, semences) ; il a ensuite nuancé sa position (Curd, 2007, p. 156, n. 8). Marmodoro comprend les opposés comme des puissances et tient les substances pour réductibles aux opposés.</ref>.
=== L'interprétation « expansive » et le témoignage d'Aristote ===
La question devient plus complexe lorsqu'on prend en compte les témoignages indirects. Aristote attribue à Anaxagore une doctrine des « homéomères » (τὰ ὁμοιομερῆ), substances dont les parties portent le même nom que le tout, comme la chair, l'os ou la moelle<ref>Aristote, ''Génération et corruption'', I, 1, 314a18-20 (DK 59 A 46) ; ''Du ciel'', III, 3, 302a28-b4 (DK 59 A 43) ; ''Physique'', I, 4, 187a25-26.</ref>.
Le terme d'homéomère n'apparaît pourtant dans aucun fragment. Il appartient au vocabulaire d'Aristote, pour qui les homéomères constituent un niveau intermédiaire entre les éléments et les organes<ref>Aristote, ''Parties des animaux'', II, 1, 646a12-24 ; Curd, 2007, p. 147-150. Burnet (1930, chap. VI) jugeait étrange, si Anaxagore avait employé le terme, que Simplicius ne cite aucun fragment qui le contienne.</ref>. Curd montre en outre qu'Aristote ne prête pas à Anaxagore un « principe d'homéomérie » au sens strict, et qu'il se trompe en faisant de l'air et du feu des mélanges d'homéomères<ref>Curd, 2007, p. 148-150.</ref>. D'autres interprètes réhabilitent au contraire la notion : William E. Mann en fait le centre de son interprétation, et John Sisko juge l'homéomérie compatible avec les autres principes d'Anaxagore et probablement authentique<ref>William E. Mann, « Anaxagoras and the ''Homoiomerē'' », ''Phronesis'', vol. 25, 1980, p. 228-249, ici p. 228 et 231-233 ; John E. Sisko, « Anaxagoras on Matter, Motion, and Multiple Worlds », ''Philosophy Compass'', vol. 5, 2010, p. 443-454, ici p. 443.</ref>.
Une lecture « expansive » soutient néanmoins que tout ce qui apparaît dans le monde sensible se trouvait déjà dans le mélange originel. Elle a été défendue, sous des formes diverses, par Strang, Stokes, Guthrie, Barnes, Furth et Graham ; Peck en proposait une version qui écartait les substances inorganiques<ref>Curd, 2007, p. 154-156 et n. 3-4, p. 158-159, n. 12 ; Curd et Sisko, ''SEP'', 2026, § 3.2 ; Arthur L. Peck, « Anaxagoras: Predication as a Problem in Physics », ''Classical Quarterly'', vol. 25, 1931, p. 27-37 et 112-120, ici p. 30, où les substances organiques sont tenues pour élémentaires et les substances inorganiques pour dérivées. Kerferd (1969) admet une forme d'homéomérie compatible avec le mélange universel (Curd et Sisko, § 3.7).</ref>. Gershenson et Greenberg proposent de leur côté une lecture particulaire dans laquelle les tissus organiques, conçus comme des « molécules » infinitésimales, sont les éléments de toute matière<ref>Daniel E. Gershenson et Daniel A. Greenberg, ''Anaxagoras and the Birth of Scientific Method'', New York, Blaisdell, 1964, p. 9-12 et 14-22.</ref>.
=== L'interprétation modérée ===
Une troisième voie a été proposée par Patricia Curd, dont la position est, selon elle, proche de celle de William E. Mann, sauf pour les semences, et a été reprise par John Sisko<ref>Curd, 2007, p. 157-171 et n. 10 ; Mann, 1980 ; Curd et Sisko, ''SEP'', 2026, § 3.2.</ref>. Selon cette lecture, les ingrédients comprennent les opposés et diverses substances : terre, air, éther, eau, feu, métaux, chair, sang, os. En revanche, les plantes, les animaux et leurs organes ne sont pas des ingrédients primordiaux, mais des composés temporaires, des « artefacts naturels » selon l'expression de Curd<ref>Curd, 2007, p. 157-163 et 170-171.</ref>.
Cette lecture tient compte à la fois des fragments et des témoignages d'Aristote. Elle reconnaît le rôle cosmologique des opposés (B12, B15) tout en admettant que d'autres substances figurent au même niveau dans le mélange : l'air et l'éther recouvrent tout avant même la rotation (B1), et B4b mentionne la terre et les semences à côté des opposés<ref>Curd, 2007, p. 166-167.</ref>.
=== Les semences (σπέρματα) ===
Les « semences » (σπέρματα), mentionnées dans les fragments B4a et B4b, ajoutent une difficulté supplémentaire. Selon B4b, avant toute séparation, aucune couleur n'était manifeste, car le mélange de toutes choses l'empêchait, « la terre étant présente en abondance et des semences illimitées en nombre, en rien semblables les unes aux autres » (καὶ γῆς πολλῆς ἐνεούσης καὶ σπερμάτων ἀπείρων πλῆθος οὐδὲν ἐοικότων ἀλλήλοις)<ref>Anaxagore, fragment B4b, cité par Simplicius, ''Commentaire sur la Physique'', 34, 21-26 (DK 59 B 4b). Sur la division de DK B4 en B4a et B4b, voir Curd, 2007, p. 42.</ref>. Le fragment B4a déclare : « Puisqu'il en est ainsi, il faut penser qu'il y a beaucoup de choses de toutes sortes dans toutes les choses qui se composent, et des semences de toutes choses, ayant des formes, des couleurs et des saveurs de toute espèce »<ref>Anaxagore, fragment B4a, cité par Simplicius, ''Commentaire sur la Physique'', 34, 29-35, 9 (DK 59 B 4a). Simplicius précise que le passage se trouvait peu après le début du livre.</ref>.
La nature de ces semences est débattue. Gregory Vlastos voyait dans σπέρμα un terme technique désignant un agrégat de tous les ingrédients dans lequel l'un d'eux prédomine ; G. E. R. Lloyd et W. K. C. Guthrie ont adopté des positions voisines<ref>Vlastos, 1950, repris dans Furley et Allen (éd.), 1975, vol. II, p. 323-353, ici p. 324 ; G. E. R. Lloyd, ''Polarity and Analogy'', Cambridge, Cambridge University Press, 1966, p. 246-247 ; Guthrie, 1965, p. 298-300. Voir Schofield, 1980, p. 123 et n. 38-39.</ref>. Une lecture aujourd'hui répandue, défendue par David Furley, Malcolm Schofield, Patricia Curd, David Sedley et Anna Marmodoro, comprend les semences au sens biologique ordinaire, comme les germes à partir desquels croissent plantes et animaux<ref>David J. Furley, « Anaxagoras in Response to Parmenides », ''Canadian Journal of Philosophy'', suppl. vol. 2, 1976, p. 61-85 ; Schofield, 1980, p. 123-126 ; Curd, 2007, p. 171-177 ; Sedley, 2007 ; Marmodoro, 2017, chap. 5.</ref>. D'autres en font des « homoncules » préformés de tous les organismes à venir (Eric Lewis), ou encore des « points de croissance » dont le développement serait réglé d'avance par le Noûs (Teodorsson)<ref>Eric Lewis, « Anaxagoras and the Seeds of a Physical Theory », ''Apeiron'', vol. 33, 2000, p. 1-23, ici p. 1 et 16-19 ; Teodorsson, 1982, p. 80-91, qui recense aussi les interprétations antérieures (p. 45-64). David Sider juge l'hypothèse de Teodorsson ingénieuse mais insuffisamment argumentée (compte rendu de Teodorsson, 1982, ''Classical Journal'', vol. 80, 1984, p. 71-73, ici p. 72). Critique de la lecture en homoncules chez Mann, 1980, p. 236, et Curd, 2007, p. 172-175.</ref>. Aristote, pour sa part, semble avoir rangé sous ce terme les homéomères eux-mêmes<ref>Aristote, ''Du ciel'', III, 3, 302a28-b4 (DK 59 A 43) ; ''Génération et corruption'', I, 1, 314a28-b1 ; Curd, 2007, p. 151-152 ; Schofield, 1980, p. 128-132.</ref>.
La lecture biologique ne fait pas violence au langage : le mot est employé dans son sens ordinaire, dans un contexte (B4a) où il est question d'hommes, d'animaux et de cultures. Elle s'accorde avec d'autres témoignages. Théophraste rapporte qu'Anaxagore disait l'air porteur de semences de toutes choses, qui, entraînées avec l'eau de pluie, engendrent les plantes<ref>Théophraste, ''Recherches sur les plantes'', III, 1, 4 (DK 59 A 117 ; cf. ''Causes des plantes'', I, 5, 2) ; Irénée, ''Contre les hérésies'', II, 14, 2 (DK 59 A 113) ; Schofield, 1980, p. 124-126.</ref>. Diogène Laërce rapporte que les animaux naquirent d'abord de l'humide, du chaud et du terreux, puis les uns des autres<ref>Diogène Laërce, II, 9 (DK 59 A 1) ; cf. Hippolyte, ''Réfutation de toutes les hérésies'', I, 8, 12 (DK 59 A 42) : les animaux naquirent d'abord dans l'humide, puis les uns des autres.</ref>.
== L'état originel : tout ensemble ==
Le traité d'Anaxagore s'ouvrait sur l'une des déclarations les plus célèbres de la philosophie présocratique : « Toutes choses étaient ensemble » (ὁμοῦ χρήματα πάντα ἦν)<ref>Anaxagore, fragment B1, cité par Simplicius, ''Commentaire sur la Physique'', 155, 26-30 (DK 59 B 1 ; LM 25 D9). Diels et Kranz impriment ὁμοῦ πάντα χρήματα ἦν ; Wolfgang Rösler a montré que les deux citations les plus complètes de Simplicius (''Commentaire sur la Physique'', 155, 26-30 ; ''Commentaire sur le Ciel'', 608, 21-23) portent ὁμοῦ χρήματα πάντα ἦν, ordre retenu par Sider et par Curd (Rösler, « ΟΜΟΥ ΧΡΗΜΑΤΑ ΠΑΝΤΑ ΗΝ », ''Hermes'', vol. 99, 1971, p. 246-248 ; Curd, 2007, p. 33 ; Schofield, 1980, p. 151, n. 1). La phrase citée par Diogène Laërce (II, 6), « Toutes choses étaient ensemble ; puis l'Intellect vint et les ordonna », est un résumé et non un fragment (Burnet, 1930, chap. VI).</ref>. Cette formule d'ouverture exprime la thèse cosmogonique d'Anaxagore sur l'état primordial de l'univers ; Schofield souligne que le livre commençait sans préambule personnel, en exposant d'emblée ce thème<ref>Schofield, 1980, p. 36-40.</ref>.
La formulation d'Anaxagore répond à Parménide<ref>Owen, 1966, repris dans Mourelatos (éd.), 1974, p. 276-277 ; Schofield, 1980, p. 64.</ref>. Là où Parménide affirmait que l'être « est maintenant tout entier ensemble, un, continu » (νῦν ἔστιν ὁμοῦ πᾶν, ἕν, συνεχές)<ref>Parménide, B 8, 5-6 (DK 28 B 8).</ref>, Anaxagore proclame que « toutes choses étaient ensemble ». À l'unité, à la continuité et au présent intemporel de l'être parménidien, il oppose la pluralité, la divisibilité illimitée, un état passé et un devenir cosmogonique<ref>Schofield, 1980, p. 64-65 ; Curd, 2007, p. 153-154.</ref>.
=== La description du mélange originel ===
Le fragment B1, que Simplicius dit placé au début du premier livre, décrit cet état primordial :
<blockquote>Toutes choses étaient ensemble, illimitées en quantité et en petitesse, car le petit aussi était illimité. Et toutes choses étant ensemble, rien n'était manifeste en raison de la petitesse ; car l'air et l'éther recouvraient toutes choses, étant l'un et l'autre illimités : ce sont eux, en effet, les plus grands dans l'ensemble des choses, et en quantité et en grandeur.<ref>Anaxagore, fragment B1 (DK 59 B 1). Le verbe κατεῖχεν est rendu ici par « recouvraient » ; Burnet le traduisait par « prédominaient », et David Sider a défendu cette lecture : comme toute chose se caractérise par ce qui prédomine en elle (B12), le mélange originel aurait présenté l'aspect de l'air et de l'éther, qui y prédominaient (« A Note on Anaxagoras, Fr. 1 », 1973, p. 249-251). Gigon jugeait cette traduction difficile sur le plan lexical.</ref></blockquote>
Quatre caractéristiques du mélange originel y sont énoncées : toutes choses étaient ensemble ; elles étaient illimitées en quantité et en petitesse ; rien n'était manifeste en raison de la petitesse ; l'air et l'éther recouvraient toutes choses.
L'expression « illimitées en quantité et en petitesse » a donné lieu à deux lectures principales, que Schofield a nommées interprétation « particulaire » et interprétation « proportionnelle »<ref>Schofield, 1980, p. 70-79.</ref>. La première comprend que le mélange originel contenait une infinité de petites particules distinctes<ref>Vlastos, 1950 ; Guthrie, 1965, p. 289 ; Kerferd, 1969 ; Sider, 1981 (voir Curd, 2007, p. 183, n. 9 ; Curd et Sisko, ''SEP'', 2026, § 3.3).</ref>. La seconde refuse de concevoir le mélange comme une collection de particules : chaque ingrédient y est présent en une proportion aussi faible qu'on voudra par rapport à l'ensemble<ref>Schofield, 1980, p. 73-79 ; Barnes, 1982, p. 323-326 ; Inwood, 1986, p. 17-18. Curd (2007, p. 181-187) propose un modèle de « densités » : les ingrédients, comparables à des liquides ou à des pâtes, sont présents partout à des concentrations variables.</ref>. Le débat n'est pas tranché<ref>Pour une discussion d'ensemble, voir Curd, 2007, p. 181-191, et Marmodoro, 2017, chap. 4 (p. 105-127).</ref>.
=== L'imperceptibilité du mélange ===
La troisième caractéristique du mélange originel est son indistinction : « rien n'était manifeste » (οὐδὲν ἔνδηλον ἦν). Anaxagore l'explique par deux facteurs : la petitesse (ὑπὸ σμικρότητος) et le fait que « l'air et l'éther recouvraient toutes choses » (πάντα γὰρ ἀήρ τε καὶ αἰθὴρ κατεῖχεν).
L'air (ἀήρ) désigne ici, selon l'usage ionien ancien, une brume sombre, humide, froide et dense ; l'éther (αἰθήρ), la substance brillante, chaude, sèche et rare, qu'Aristote dit identifiée au feu par Anaxagore<ref>Théophraste, ''Du sens'', 59 (DK 59 A 70) : le rare et le fin sont chauds, le dense et l'épais froids, comme Anaxagore définit l'éther et l'air ; Aristote, ''Du ciel'', I, 3, 270b24-25 (DK 59 A 73), et III, 3, 302b4 ; Schofield, 1980, p. 71. Peter Kingsley souligne qu'il s'agit d'une interprétation d'Aristote, plus prudent dans les ''Météorologiques'' : chez Anaxagore, l'éther serait plutôt un air sec qui tend vers le haut, dont le feu dérive comme l'eau et la terre dérivent de l'air humide (« Notes on Air: Four Questions of Meaning in Empedocles and Anaxagoras », ''Classical Quarterly'', n.s. vol. 45, 1995, p. 26-29, ici p. 28-29).</ref>. Ces deux substances, dit B1, sont les plus grandes dans l'ensemble des choses, en quantité et en grandeur : elles prédominaient dans le mélange originel, à la manière d'un brouillard qui recouvre tout<ref>Curd, 2007, p. 178 et n. 1 ; Schofield, 1980, p. 155-156, n. 5-6.</ref>.
Le fragment B4b confirme cette description :
<blockquote>Mais avant que ces choses ne fussent séparées, toutes étant ensemble, aucune couleur n'était manifeste ; car le mélange de toutes choses l'empêchait, celui de l'humide et du sec, du chaud et du froid, du brillant et de l'obscur, la terre étant présente en abondance et des semences illimitées en nombre, en rien semblables les unes aux autres.<ref>Anaxagore, fragment B4b (DK 59 B 4b).</ref></blockquote>
L'indistinction du mélange originel ne tenait donc pas à l'absence des ingrédients, mais à leur mélange si intime qu'aucun ne pouvait se manifester. Curd souligne qu'il s'agit d'un contrefactuel : aucun observateur n'était présent, mais un observateur n'aurait rien pu y distinguer<ref>Curd, 2007, p. 46.</ref>.
=== L'immobilité originelle ===
Plusieurs témoignages rapportent qu'avant l'intervention du Noûs, le mélange était au repos depuis un temps illimité<ref>Aristote, ''Physique'', VIII, 1, 250b24-26 ; Simplicius, ''Commentaire sur la Physique'', 1121, 21 (DK 59 A 64) ; Aétius, I, 7, 5 (DK 59 A 48).</ref>. Cette immobilité pose une difficulté : si le mélange était au repos, qu'est-ce qui a pu le mettre en mouvement ? Anaxagore répond en posant le Noûs (Νοῦς, Intellect), distinct de tous les ingrédients, qui possède le pouvoir d'initier le mouvement<ref>Anaxagore, B12 et B13.</ref>. Eudème reprochait déjà à Anaxagore de faire commencer le mouvement à un moment donné sans dire s'il cesserait un jour<ref>Simplicius, ''Commentaire sur la Physique'', 1185, 9 (DK 59 A 59). Une colonne d'un papyrus d'Herculanum (Philodème) attribue au contraire à Anaxagore l'idée d'un mouvement éternel ; voir Christian Vassallo, ''The Presocratics at Herculaneum'', Berlin et Boston, De Gruyter, 2021, et Curd et Sisko, ''SEP'', 2026, § 4.4.</ref>.
=== L'étendue du mélange originel ===
Le fragment B1 affirme que l'air et l'éther « étaient l'un et l'autre illimités » (ἀμφότερα ἄπειρα ἐόντα), et le fragment B2 précise que « l'air et l'éther se séparent de la masse environnante, et la masse environnante est illimitée en quantité »<ref>Anaxagore, fragment B2 (DK 59 B 2).</ref>. Le mélange originel était donc spatialement illimité<ref>Aristote, ''Physique'', III, 4, 203a19-33 (DK 59 A 45) ; III, 5, 205b1-5 (DK 59 A 50), où Aristote critique l'idée que l'illimité se fixe lui-même en place.</ref>. Le fragment B12 indique que la révolution a commencé à partir d'une petite région, qu'elle s'étend maintenant davantage et qu'elle s'étendra davantage encore<ref>Anaxagore, B12 (DK 59 B 12).</ref>. Le processus cosmogonique n'a donc pas encore affecté la totalité du mélange : la description de B1 vaut toujours pour les régions que la rotation n'a pas atteintes<ref>Curd, 2007, p. 207-208.</ref>. Cette conception d'un univers partiellement ordonné, dont la formation se poursuit à la périphérie, compte parmi les idées les plus originales d'Anaxagore.
== Le Noûs : l'Intellect cosmique ==
Le fragment B12, le plus long des fragments conservés, est presque entièrement consacré au Noûs (Νοῦς, « Intellect » ou « Esprit »). Schofield y voit l'un des passages les plus puissants de toute la prose grecque, par son intensité et sa lente grandeur<ref>Schofield, 1980, p. 4 ; sur le style de « prédication solennelle », voir Deichgräber, 1933, et Schofield, 1980, p. 6-9.</ref>. Anaxagore y expose la nature du Noûs et son rôle dans la formation du monde. La tradition a souvent tenu cette doctrine pour son apport le plus original ; Diogène Laërce et Plutarque rapportent qu'on le surnommait lui-même « Noûs », surnom qui pouvait être moqueur autant qu'admiratif<ref>Diogène Laërce, II, 6 (DK 59 A 1) ; Plutarque, ''Vie de Périclès'', 4 (DK 59 A 15) ; Curd, 2007, p. 192. Burnet (1930, chap. VI) jugeait au contraire que l'originalité d'Anaxagore tenait davantage à sa théorie de la substance qu'à celle du Noûs. Walter Bröcker constatait ce partage : Zeller et Capelle voyaient l'apport d'Anaxagore dans sa doctrine de l'intellect, Tannery et Burnet dans sa théorie de la matière (« Die Lehre des Anaxagoras », ''Kant-Studien'', vol. 42, 1942-1943, p. 176-189, ici p. 176).</ref>.
Deux points doivent être distingués. Le Noûs est d'abord la source du mouvement : il initie et contrôle la révolution qui produit la séparation des ingrédients à partir du mélange originel. Il est aussi un intellect : il connaît, discerne et ordonne. En revanche, aucun fragment conservé n'affirme que l'Intellect dispose les choses de la manière qui est la meilleure. C'est précisément ce que Socrate, dans le ''Phédon'', aurait voulu trouver chez Anaxagore, et dont il déplore l'absence<ref>Platon, ''Phédon'', 97b-98c (DK 59 A 47) ; Curd, 2007, p. 144 et 192.</ref>. L'identification du Noûs à une cause finale organisant le monde en vue du bien doit donc beaucoup à la lecture de Platon, puis d'Aristote.
Appliquer aux présocratiques la distinction aristotélicienne des quatre causes (matérielle, formelle, efficiente, finale) est par ailleurs anachronique : cette classification est une construction d'Aristote, élaborée pour situer ses prédécesseurs par rapport à sa propre doctrine<ref>Patricia Curd, « Presocratic Philosophy », ''Stanford Encyclopedia of Philosophy'', 2007, révision substantielle 2020, § 2 ; Curd, 2007, p. 141, n. 32, et p. 198, où elle qualifie elle-même d'anachronique l'expression de « cause efficiente ».</ref>. Le Noûs ne se laisse ranger entièrement dans aucune de ces catégories. La question de savoir s'il poursuit des fins, et en quel sens, fait l'objet d'un débat qui est présenté plus loin.
=== La séparation du Noûs d'avec toutes choses ===
Le fragment B12 s'ouvre par une affirmation qui constitue la thèse centrale d'Anaxagore concernant le Noûs :
<blockquote>Les autres choses ont part à toute chose, mais l'Intellect est illimité et maître de lui-même, il n'est mêlé à aucune chose, mais il est seul, lui-même par lui-même.<ref>Anaxagore, fragment B12, cité par Simplicius, ''Commentaire sur la Physique'', 156, 13-15 (DK 59 B 12). Simplicius cite la première proposition en 164, 24-25.</ref></blockquote>
Le Noûs fait ainsi exception au principe du mélange universel : alors que tous les ingrédients contiennent des parts de tous les autres, lui seul demeure pur et séparé<ref>Anaxagore, B11 (DK 59 B 11).</ref>. Le fragment B14 affirme pourtant qu'il est là où sont toutes les autres choses : dans la masse environnante, dans ce qui s'est adjoint et dans ce qui s'est séparé<ref>Anaxagore, B14, cité par Simplicius, ''Commentaire sur la Physique'', 157, 5-7 (DK 59 B 14). Le début du fragment est altéré ; voir David Sider, « Anaxagoras Fr. 14 DK », ''Hermes'', vol. 102, 1974, p. 365-367.</ref>. Trois attributs lui sont d'abord prédiqués : il est « illimité » (ἄπειρον), « maître de lui-même » (αὐτοκρατές) et « mêlé à aucune chose ». Anaxagore justifie cette séparation par un argument :
<blockquote>Car s'il n'était pas par lui-même, mais s'il était mêlé à quelque autre chose, il aurait part à toutes choses, s'il était mêlé à l'une d'elles ; car en toute chose il y a une part de toute chose, comme je l'ai dit auparavant. Et les choses mêlées à lui l'empêcheraient, de sorte qu'il ne dominerait aucune chose comme il le fait, étant seul par lui-même.<ref>Anaxagore, B12, cité par Simplicius, ''Commentaire sur la Physique'', 156, 15-20.</ref></blockquote>
Il s'agit d'une preuve indirecte : si le Noûs était mêlé à quoi que ce soit, il serait mêlé à tout ; les choses mêlées à lui l'empêcheraient d'exercer son pouvoir ; or il l'exerce ; il n'est donc mêlé à rien<ref>Schofield, 1980, p. 7 et 19, qui voit dans l'usage de cette forme d'argument un possible indice d'influence éléate (p. 146, n. 15).</ref>. Anaxagore n'explique pas pourquoi le mélange entraverait l'action du Noûs, et plusieurs explications ont été proposées<ref>Curd, 2007, p. 58-59 et 200-201 ; Schofield, 1980, p. 147-148, n. 39.</ref>.
=== Les attributs du Noûs ===
Après avoir établi la séparation du Noûs, Anaxagore énonce une série d'attributs dans le style de la prédication solennelle, qu'Eduard Norden et Karl Deichgräber ont rapproché de l'hymne religieux<ref>Deichgräber, 1933, p. 347-361 ; Eduard Norden, ''Agnostos Theos'', Leipzig, Teubner, 1913 (4{{e}} tirage, Stuttgart, 1956), p. 143-176 ; Schofield, 1980, p. 6-9 et 12.</ref> :
<blockquote>Car il est la plus fine de toutes les choses et la plus pure ; il détient tout discernement (γνώμη) sur toute chose et il a la plus grande force ; et toutes les choses qui ont une âme, les plus grandes comme les plus petites, l'Intellect les domine.<ref>Anaxagore, B12, cité par Simplicius, ''Commentaire sur la Physique'', 156, 20-24.</ref></blockquote>
Le premier attribut, qui qualifie le Noûs de « plus fin » (λεπτότατον) et « plus pur » (καθαρώτατον), a reçu des interprétations divergentes, que Schofield ramène à trois<ref>Schofield, 1980, p. 11-12.</ref>. Pour les uns, il s'agit d'une substance matérielle d'une extrême finesse : Anaxagore emploie probablement λεπτός en un sens physique à propos de l'eau de mer, et il parle d'un intellect « plus grand » ou « plus petit »<ref>Aétius, III, 16, 2 (DK 59 A 90) ; Burnet, 1930, p. 268 ; J. E. Raven, « The Basis of Anaxagoras' Cosmology », ''Classical Quarterly'', n.s. vol. 4, 1954, p. 123-137, ici p. 134-135 ; Barnes, 1982, p. 406-409 ; Sider, 1981. Voir Schofield, 1980, p. 147, n. 28, et Curd, 2007, p. 59, n. 50.</ref>. Pour d'autres, ces termes visent à exprimer l'incorporéité du Noûs<ref>Guthrie, 1965, p. 276-278 ; Curd, 2007, p. 59 et 200 ; Gershenson et Greenberg, 1964, p. 32-33, qui font d'Anaxagore le premier penseur grec à introduire une entité substantielle incorporelle.</ref>. Une troisième lecture, celle d'Aristote, à laquelle Schofield se rallie, comprend que le Noûs n'a aucun caractère propre qui le rendrait semblable aux choses qu'il connaît et domine<ref>Aristote, ''De l'âme'', III, 4, 429a18-24 (DK 59 A 100) ; Schofield, 1980, p. 11-12.</ref>. On a longtemps soutenu que la notion d'incorporéité n'était pas encore disponible à l'époque d'Anaxagore ; Zeller jugeait qu'il avait voulu parler d'un incorporel sans y parvenir<ref>Burnet, 1930, chap. VI, qui rapporte la position de Zeller ; Raven, 1954, p. 130, pour qui aucun présocratique n'avait encore saisi l'existence de l'incorporel.</ref>. Curd rappelle toutefois que Mélissos refuse déjà un corps à l'Un<ref>Mélissos, DK 30 B 9 ; Curd, 2007, p. 59.</ref>.
Le deuxième attribut concerne la connaissance : le Noûs « détient tout discernement sur toute chose ». Le troisième concerne la puissance : il « a la plus grande force ». Anaxagore suggère le lien entre les deux par une assonance, ἴσχει (« il détient ») et ἰσχύει (« il a de la force »)<ref>Schofield, 1980, p. 15 et p. 147, n. 36 ; Curd, 2007, p. 60. James Lesher souligne que γνώμη unit connaissance et détermination : le Noûs juge et décide de toutes choses, mais à partir d'un savoir (« Mind's Knowledge and Powers of Control in Anaxagoras DK B12 », ''Phronesis'', vol. 40, 1995, p. 125-142, ici p. 138-141).</ref>. Le quatrième attribut, le contrôle exercé sur tous les êtres animés, s'accorde avec B11, selon lequel l'Intellect est présent dans certaines choses. Schofield montre que tout le passage peut se lire aussi bien comme une description d'un Intellect suprême que comme une thèse sur l'intellect en général, et que cette ambiguïté est probablement inhérente au texte<ref>Schofield, 1980, p. 10-22 ; Curd, 2007, p. 60-61.</ref>.
=== Le rôle cosmogonique du Noûs ===
Après avoir décrit la nature du Noûs, Anaxagore expose son rôle dans la cosmogonie :
<blockquote>Et l'Intellect a dominé la révolution entière, de sorte qu'elle a commencé à tourner au commencement. Elle a d'abord commencé à tourner à partir d'une petite région, mais elle tourne sur une région plus grande, et elle tournera sur une région plus grande encore. Et les choses qui se mêlent, qui se séparent et qui se dissocient, l'Intellect les a toutes connues. Et celles qui devaient être, celles qui étaient et ne sont plus, celles qui sont maintenant et celles qui seront, l'Intellect les a toutes ordonnées, ainsi que cette révolution dans laquelle tournent maintenant les astres, le soleil, la lune, l'air et l'éther qui se séparent.<ref>Anaxagore, B12, cité par Simplicius, ''Commentaire sur la Physique'', 156, 24-157, 2.</ref></blockquote>
Ce passage affirme trois choses. L'Intellect initie une révolution (περιχώρησις) dans un mélange jusqu'alors immobile. Ce mouvement, commencé dans une petite région, s'étend progressivement et continue de s'étendre : la cosmogonie n'est pas un événement révolu, mais un processus qui se poursuit à la périphérie du mélange illimité. Enfin, c'est ce mouvement qui produit la séparation (ἀπόκρισις) et la dissociation (διάκρισις) des ingrédients, d'où naît le cosmos ordonné que nous observons<ref>Anaxagore, B12 et B13 ; Simplicius, ''Commentaire sur la Physique'', 300, 27-301, 1 ; Aristote, ''Physique'', VIII, 1, 250b24-26.</ref>.
Le mécanisme de la séparation est d'ordre physique : la rotation, par sa vitesse et sa force, rassemble au centre les ingrédients denses, humides, froids et obscurs, et repousse vers la périphérie les ingrédients rares, chauds, secs et brillants<ref>Anaxagore, B9, B12, B15 ; Aristote, ''Du ciel'', II, 13, 295a9-14 (DK 59 A 88), qui rapporte que, dans les tourbillons, les corps les plus lourds se portent vers le centre ; Curd, 2007, p. 207-208.</ref>. Le Noûs n'intervient donc pas dans chaque détail : il initie et gouverne la rotation, qui produit ensuite la séparation et la recomposition des ingrédients. C'est pourquoi Platon et Aristote pourront reprocher à Anaxagore de ne plus recourir à l'Intellect dans l'explication des phénomènes particuliers. Simplicius répondait déjà que le Noûs demeure la cause première, puisque la génération est séparation, que la séparation résulte du mouvement et que le Noûs est cause du mouvement<ref>Simplicius, ''Commentaire sur la Physique'', 300, 27-31 ; Curd, 2007, p. 202-203.</ref>.
Le texte affirme aussi que « l'Intellect a ordonné » (διεκόσμησε νοῦς) toutes choses, passées, présentes et futures. Le verbe διακοσμεῖν signifie « disposer », « mettre en ordre ». Le Noûs connaît et dispose ; il est à la fois intelligent et ordonnateur. Mais aucun fragment n'explique cet ordre par le meilleur, et la portée téléologique de la doctrine reste débattue. De nombreux interprètes contemporains attribuent au Noûs une forme de téléologie : une téléologie « mince », qui lie sa connaissance à son action (Lesher, Curd, Pinto), ou « épaisse », qui lui prête des buts déterminés, comme la connaissance par discernement des ingrédients (André Laks) ou la production du monde le plus favorable à la vie humaine (David Sedley)<ref>Curd et Sisko, ''SEP'', 2026, § 4.2 ; Lesher, 1995 ; Curd, 2007, p. 194 et 204-205 ; Rhodes Pinto, « ''Nous'', Motion, and Teleology in Anaxagoras », ''Oxford Studies in Ancient Philosophy'', vol. 52, 2017, p. 1-32 ; André Laks, « Mind's Crisis: On Anaxagoras' ''Nous'' », ''Southern Journal of Philosophy'', vol. 31, suppl., 1993, p. 19-38 ; Sedley, 2007. Anna Marmodoro voit en Anaxagore le premier à proposer une approche téléologique de la cosmologie, le Noûs développant le monde selon sa conception de l'ordre à partir de ce qui est donné (Marmodoro, 2017, p. 129-130).</ref>. D'autres comprennent le Noûs comme une loi de la nature plutôt que comme un agent qui poursuit des fins<ref>Sider, 1981, selon Curd et Sisko, ''SEP'', 2026, § 4.1-4.2 ; Gershenson et Greenberg, 1964, p. 25 ; John E. Sisko, « Anaxagoras betwixt Parmenides and Plato », ''Philosophy Compass'', vol. 5, 2010, p. 432-442, qui estime qu'aucun argument probant n'a été opposé à la lecture de Platon.</ref>. Curd, pour sa part, nie que le Noûs soit un principe téléologique extérieur fixant un plan au cosmos, tout en laissant ouverte la possibilité d'un ordre immanent aux processus qu'il déclenche<ref>Curd, 2007, p. 144 et 204-205.</ref>. Kurt von Fritz voyait dans le Noûs la jonction mal accordée de deux héritages, l'intelligence cognitive venue de Parménide et le principe moteur des Milésiens ; André Laks résume cette lecture par la formule paradoxale d'une « intelligence aveugle » et assigne au Noûs un but premier, séparer du mélange des entités distinctes, interprétation que discute Joseph DeFilippo<ref>Kurt von Fritz, « Der ΝΟΥΣ des Anaxagoras », ''Archiv für Begriffsgeschichte'', vol. 9, 1964, p. 87-102, repris dans ''Grundprobleme der Geschichte der antiken Wissenschaft'', Berlin et New York, De Gruyter, 1971, p. 576-593 ; Joseph G. DeFilippo, « Reply to André Laks on Anaxagoras' ΝΟΥΣ », ''Southern Journal of Philosophy'', vol. 31, suppl., 1993, p. 39-48, ici p. 39-40 et 46 ; Curd et Sisko, ''SEP'', 2026, § 4.2.</ref>.
=== La critique platonicienne et aristotélicienne ===
Dans le ''Phédon'', Platon fait raconter par Socrate sa déception à la lecture du livre d'Anaxagore :
<blockquote>Un jour, j'entendis quelqu'un lire dans un livre d'Anaxagore, disait-il, que c'est l'Intellect qui met tout en ordre et qui est la cause de toutes choses. Cette cause me réjouit, et il me sembla qu'il était bon, d'une certaine manière, que l'Intellect fût la cause de tout ; et je pensai que, s'il en est ainsi, l'Intellect, en ordonnant, ordonne tout et dispose chaque chose de la manière qui est la meilleure. [...] Mais cette merveilleuse espérance, mon ami, me fut ôtée lorsque, poursuivant ma lecture, je vis un homme qui ne faisait aucun usage de l'Intellect, qui ne lui attribuait aucune responsabilité dans l'ordonnance des choses, mais qui alléguait comme causes des airs, des éthers, des eaux et bien d'autres choses étranges.<ref>Platon, ''Phédon'', 97b-98c (DK 59 A 47).</ref></blockquote>
La portée de ce passage doit être saisie exactement. Socrate ne dit pas qu'Anaxagore avait proposé une explication par le meilleur et l'avait mal développée : il dit qu'il ''espérait'' en trouver une, et qu'il fut déçu. La formule « disposer chaque chose de la manière qui est la meilleure » exprime l'attente socratique, non la doctrine d'Anaxagore.
Le texte d'Anaxagore, dans les fragments que nous possédons, affirme que le Noûs connaît toutes choses et qu'il les ordonne par l'intermédiaire de la révolution cosmique. Il n'affirme pas que cet ordre soit le meilleur possible. C'est Socrate qui, lisant Anaxagore, infère que, si le monde est ordonné par un Intellect, il doit l'être en vue du bien. Curd juge que Platon a raison de constater l'absence, chez Anaxagore, d'un bien indépendant qui servirait de principe d'explication ; elle note que cette exigence, chez Platon, conduira à la Forme du Bien, puis au Démiurge du ''Timée''<ref>Curd, 2007, p. 144-145 et 204.</ref>. Il faut donc distinguer l'attribution au Noûs d'un rôle moteur et cognitif, qui est anaxagoréenne, et celle d'un principe évaluatif selon lequel le monde serait disposé en vue du meilleur, qui ne se trouve pas dans les fragments. Le reproche socratique ne dénonce pas une incohérence interne ; il regrette qu'Anaxagore ne soit pas allé jusqu'à l'explication que Socrate attendait.
Aristote reprend, dans la ''Métaphysique'', le reproche d'un usage insuffisant du Noûs :
<blockquote>Anaxagore se sert de l'Intellect comme d'un ''deus ex machina'' pour la fabrication du monde ; et quand il est embarrassé pour dire par quelle cause une chose est nécessairement, il le fait intervenir ; mais dans les autres cas, il donne pour causes de ce qui arrive toutes choses plutôt que l'Intellect.<ref>Aristote, ''Métaphysique'', A, 4, 985a18-21 (DK 59 A 47).</ref></blockquote>
Aristote précise ailleurs que ceux qui posent l'Intellect ou l'Amitié comme causes les traitent comme des principes du mouvement plutôt que comme des fins<ref>Aristote, ''Métaphysique'', A, 7, 988b6-16.</ref>. Ses autres objections, comme le souligne Curd, ne portent pas d'abord sur l'absence de téléologie : elles visent la confusion entre l'âme et l'intellect, et l'absence d'explication de la manière dont le Noûs connaît<ref>Aristote, ''De l'âme'', I, 2, 404a25-b6 et 405a13-19 (DK 59 A 55, A 99, A 100) ; III, 4, 429a18-24 et 429b22-24 (DK 59 A 100) ; Curd, 2007, p. 146 et 205.</ref>. L'explication par le meilleur, au sens où Socrate l'attendait, ne se trouve donc pas dans les fragments d'Anaxagore ; elle se constitue dans la critique platonicienne, puis dans la doctrine aristotélicienne de la cause finale. Certains interprètes modernes n'en reconnaissent pas moins, chez Anaxagore, une première forme de téléologie cosmique.
=== Synthèse : l'innovation du Noûs ===
L'introduction du Noûs constitue une innovation à plusieurs titres.
Anaxagore est, parmi les penseurs grecs dont nous connaissons les doctrines, le premier à poser explicitement une entité qui, bien qu'elle agisse sur les ingrédients, n'est mêlée à aucun d'eux<ref>Guthrie, 1965, p. 276-279 ; Curd, 2007, p. 193-194 et 200-201. Aristote (''Métaphysique'', A, 3, 984b15-20 = DK 59 A 58) signale que la doctrine était attribuée avant lui à Hermotime de Clazomènes, figure largement légendaire (Curd, 2007, p. 205, n. 23).</ref>. Il s'inscrit néanmoins dans une tradition : l'idée d'une intelligence cosmique qui comprend et gouverne l'univers se trouve déjà chez Xénophane et Héraclite<ref>Curd, 2007, p. 195 et n. 6-7.</ref>. Sa conception préparera, sans leur être équivalente, les doctrines ultérieures de l'âme séparée chez Platon et de l'intellect séparé chez Aristote<ref>Platon, ''Phédon'', 78b-84b ; Aristote, ''De l'âme'', III, 4-5.</ref>.
Anaxagore identifie par ailleurs une cause unique du mouvement cosmique et de l'ordre qui en résulte. Aristote salue cette innovation en disant qu'Anaxagore parut « comme un homme sobre » au milieu de prédécesseurs qui parlaient au hasard<ref>Aristote, ''Métaphysique'', A, 3, 984b15-20 (DK 59 A 58).</ref>. En nommant ce principe Νοῦς, Anaxagore suggère que l'ordre du monde n'est pas aveugle. Socrate, Platon et Aristote reprendront cette suggestion en la transformant profondément, au-delà de ce qu'affirment les fragments<ref>Schofield, 1980, p. 59-61, qui juge le Noûs plus proche du Démiurge platonicien que du Dieu créateur de la tradition judéo-chrétienne ; Curd, 2007, p. 143-146.</ref>.
== La cosmogonie et la cosmologie ==
La cosmogonie et la cosmologie d'Anaxagore appliquent ses principes métaphysiques. Anaxagore s'y montre héritier de la tradition ionienne et novateur, proposant des explications naturalistes des phénomènes célestes et météorologiques<ref>Curd, 2007, p. 206-234 ; Gershenson et Greenberg, 1964, p. 34-54 ; Daniel W. Graham, ''Science before Socrates: Parmenides, Anaxagoras, and the New Astronomy'', New York, Oxford University Press, 2013.</ref>. Les témoignages sont ici notre source principale, et ils ne concordent pas toujours.
=== Le mouvement rotatoire cosmogonique ===
Le processus cosmogonique commence lorsque le Noûs imprime au mélange originel un mouvement de révolution (περιχώρησις)<ref>Anaxagore, B12 et B13 (DK 59 B 12-13).</ref>. Ce mouvement prend la forme d'un tourbillon (δίνη) dont l'étendue ne cesse de croître<ref>Aristote, ''Du ciel'', II, 13, 295a9-14 (DK 59 A 88) ; Curd, 2007, p. 207-208.</ref>. Anaxagore affirme que sa rapidité est sans commune mesure avec celle d'aucune chose connue des hommes : elle est « de beaucoup de fois plus rapide »<ref>Anaxagore, fragment B9 (DK 59 B 9).</ref>.
La séparation qui en résulte est d'ordre physique : le tourbillon rassemble au centre les ingrédients denses et lourds et repousse vers la périphérie les ingrédients rares et légers, comme on l'observe dans les tourbillons d'eau ou d'air<ref>Aristote, ''Du ciel'', II, 13, 295a9-14 (DK 59 A 88) ; Hippolyte, ''Réfutation'', I, 8, 2 (DK 59 A 42).</ref>. Le fragment B15 décrit ce processus :
<blockquote>Le dense, l'humide, le froid et l'obscur se rassemblèrent ici, là où se trouve maintenant la terre ; le rare, le chaud et le sec se retirèrent vers les régions lointaines de l'éther.<ref>Anaxagore, fragment B15, cité par Simplicius, ''Commentaire sur la Physique'', 179, 3-6 (DK 59 B 15). Hippolyte ajoute « le brillant » à la seconde série.</ref></blockquote>
Cette séparation n'est jamais achevée : conformément au principe selon lequel rien n'est complètement séparé (B8, B12), la rotation continue de produire des séparations et des mélanges partiels<ref>Anaxagore, B8 (« les choses qui sont dans l'unique cosmos ne sont pas séparées les unes des autres ni tranchées à la hache ») et B12.</ref>. L'ordre exact des premières séparations, celle de l'air et de l'éther puis celle des opposés et de la terre, reste discuté<ref>D. Bargrave-Weaver, « The Cosmogony of Anaxagoras », ''Phronesis'', vol. 4, 1959, p. 77-91 ; M. C. Stokes, « On Anaxagoras, Part II: The Order of Cosmogony », ''Archiv für Geschichte der Philosophie'', vol. 47, 1965, p. 217-250 ; Ronald Potts, « Anaxagoras' Cosmogony », ''Apeiron'', vol. 18, 1984, p. 90-96.</ref>.
=== La formation de la terre ===
Au centre du tourbillon, les ingrédients denses, humides, froids et obscurs se sont concentrés pour former la terre<ref>Anaxagore, B15 et B16 ; Hippolyte, ''Réfutation'', I, 8, 2 (DK 59 A 42).</ref>. Selon Anaxagore, la terre est plate<ref>Hippolyte, ''Réfutation'', I, 8, 3 (DK 59 A 42) ; Aristote, ''Du ciel'', II, 13, 294b13-21 (DK 59 A 87).</ref>, conception traditionnelle dans la cosmologie ionienne.
Elle demeure immobile parce qu'elle repose sur l'air qui la supporte. Selon Aristote, Anaximène, Anaxagore et Démocrite expliquaient cette stabilité par la platitude de la terre : elle ne fend pas l'air situé au-dessous, mais le recouvre comme un couvercle<ref>Aristote, ''Du ciel'', II, 13, 294b13-21 (DK 59 A 87). Hippolyte (I, 8, 3 = DK 59 A 42) invoque aussi la grandeur de la terre et l'absence de vide.</ref>. Aristote rapporte qu'Anaxagore montrait la résistance de l'air au moyen d'outres gonflées et de clepsydres<ref>Aristote, ''Physique'', IV, 6, 213a22-27 (DK 59 A 68) ; pseudo-Aristote, ''Problèmes'', XVI, 8, 914b9-915a24 (DK 59 A 69) ; Gershenson et Greenberg, 1964, p. 40-43. La clepsydre désigne ici un ustensile servant à transvaser les liquides (Curd, 2007, p. 108, n. 28).</ref>.
=== La formation des corps célestes ===
Les corps célestes se seraient formés à partir de pierres arrachées à la terre par la violence de la rotation, puis enflammées par l'éther brûlant qui les emporte<ref>Aétius, II, 13, 3 (DK 59 A 71) ; Hippolyte, ''Réfutation'', I, 8, 6 (DK 59 A 42) ; Curd, 2007, p. 209.</ref>. Les astres ne sont donc pas des êtres divins, mais des pierres incandescentes<ref>Platon, ''Apologie'', 26d (DK 59 A 35) ; Hippolyte, ''Réfutation'', I, 8, 6 (DK 59 A 42). Cette thèse est au cœur des accusations d'impiété.</ref>.
Anaxagore soutenait que le soleil est une masse de métal incandescent ou une pierre de feu, plus grande que le Péloponnèse, et même plusieurs fois plus grande selon Aétius ; Plutarque lui attribue aussi l'idée que la lune a la taille du Péloponnèse<ref>Diogène Laërce, II, 8 (DK 59 A 1) ; Hippolyte, ''Réfutation'', I, 8, 8 (DK 59 A 42) ; Aétius, II, 20, 6 et II, 21, 3 (DK 59 A 72) ; Plutarque, ''Sur la face qui paraît dans la lune'', 932a. Ces textes sont réunis par Daniel W. Graham et Eric Hintz, « Anaxagoras and the Solar Eclipse of 478 BC », ''Apeiron'', vol. 40, 2007, p. 319-344, ici p. 319.</ref>. L'estimation paraissait extravagante à une époque où l'on voyait dans le soleil un disque de dimensions modestes ; elle reste très inférieure à la taille réelle de l'astre. On a cherché l'origine de ces estimations dans l'observation d'une éclipse de soleil : David Sider, à la suite d'Erich Frank, pensait à celle du 30 avril 463 ; Daniel Graham et Eric Hintz ont proposé l'éclipse annulaire du 17 février 478, dont l'ombre couvrit une grande partie du Péloponnèse. Ce sont des hypothèses<ref>David Sider, « Anaxagoras on the Size of the Sun », ''Classical Philology'', vol. 68, 1973, p. 128-129 ; Graham et Hintz, 2007, p. 321-330 (éclipse de 478 : p. 324) ; Graham, ''Science before Socrates'', 2013 ; Curd et Sisko, ''SEP'', 2026, § 1.</ref>.
La lune, selon Anaxagore, est faite de terre, avec des plaines et des ravins<ref>Hippolyte, ''Réfutation'', I, 8, 10 (DK 59 A 42) ; Diogène Laërce, II, 8 (DK 59 A 1) ; Aétius, II, 30, 2 (DK 59 A 77), qui parle de hauteurs, de plaines et de creux.</ref>. Elle tient sa lumière du soleil<ref>Anaxagore, B18, cité par Plutarque, ''Sur la face qui paraît dans la lune'', 16, 929b (DK 59 B 18) ; Platon, ''Cratyle'', 409a-b (DK 59 A 76).</ref>. Aétius attribue toutefois cette découverte à plusieurs penseurs, depuis Thalès, Pythagore et Parménide ; Daniel Graham en crédite Parménide, et Dirk Couprie a contesté que l'expression « tenir sa lumière du soleil » désigne chez Anaxagore une simple réflexion<ref>Aétius, II, 28, 5 (DK 59 A 77) ; Parménide, DK 28 B 14-15 ; Daniel W. Graham, « La lumière de la lune dans la pensée grecque archaïque », dans André Laks et Claire Louguet (éd.), ''Qu'est-ce que la philosophie présocratique ?'', Lille, Presses universitaires du Septentrion, 2002, p. 351-380, et ''Science before Socrates'', 2013, chap. 3 (voir le compte rendu de Gerard Naddaf, ''Mind'', vol. 125, 2016, p. 945-952) ; Denis O'Brien, « Derived Light and Eclipses in the Fifth Century », ''Journal of Hellenic Studies'', vol. 88, 1968, p. 114-127 ; Dirk L. Couprie, « Anaxagoras on the Light and Phases of the Moon », ''Hyperboreus'', vol. 24, 2018, p. 12-39 ; Curd, 2007, p. 211 et n. 15.</ref>.
Hippolyte crédite Anaxagore d'avoir le premier expliqué les éclipses et les phases de la lune. Une éclipse de lune se produit lorsque la terre s'interpose entre le soleil et la lune ; une éclipse de soleil, lors de la nouvelle lune, lorsque la lune s'interpose entre le soleil et la terre<ref>Hippolyte, ''Réfutation'', I, 8, 9-10 (DK 59 A 42) ; Aétius, II, 29, 6-7 (DK 59 A 77) ; Plutarque, ''Vie de Nicias'', 23 (DK 59 A 18), qui lui attribue le premier exposé écrit de la théorie des phases.</ref>. Cette explication s'accompagne d'un élément étranger à l'astronomie moderne : certaines éclipses de lune seraient dues à des corps invisibles situés au-dessous de la lune. La plupart des historiens y voient une avancée importante de l'astronomie grecque, que Graham et Hintz jugent pour l'essentiel correcte ; Couprie soutient au contraire que l'ombre de la terre servait chez Anaxagore à expliquer la Voie lactée, et que les corps invisibles étaient sa seule explication des éclipses de lune<ref>Hippolyte, ''Réfutation'', I, 8, 6 et 9 (DK 59 A 42) ; Aétius, II, 29, 7 (DK 59 A 77) ; Curd, 2007, p. 211 ; Graham et Hintz, 2007, p. 320, qui voient aussi en lui « peut-être le premier astronome empirique » (p. 341) ; Dirk L. Couprie, « Anaxagoras on the Milky Way and Lunar Eclipses », ''Hyperboreus'', vol. 23, 2017, p. 181-207.</ref>.
La tradition prête aussi à Anaxagore des prédictions, dont celle d'une éclipse de soleil<ref>Philostrate, ''Vie d'Apollonios de Tyane'', I, 2 (DK 59 A 6) ; cf. Hippolyte, ''Réfutation'', I, 8, 13 (DK 59 A 42), qui le dit « devin ».</ref>. Les historiens modernes restent sceptiques à l'égard de ces récits<ref>Curd, 2007, p. 132, n. 9-10.</ref>. L'affirmation que la surface de la lune présente des reliefs analogues à ceux de la terre annonce, à titre d'intuition, ce que les observations de Galilée rendront visible en 1610.
La Voie lactée s'explique, selon Anaxagore, par l'ombre que projette la terre lorsque le soleil passe au-dessous d'elle : les étoiles situées dans cette ombre, que le soleil n'éclaire pas, laissent voir leur propre lumière<ref>Aristote, ''Météorologiques'', I, 8, 345a25-31 (DK 59 A 80) ; Aétius, III, 1, 5 ; Hippolyte, ''Réfutation'', I, 8, 10 (DK 59 A 42) ; Curd, 2007, p. 210 ; Couprie, 2017.</ref>.
=== La météorite d'Aigos Potamos ===
La chute d'une grande météorite à Aigos Potamos, sur la rive européenne de l'Hellespont, vers 467 av. J.-C., contribua à la réputation d'Anaxagore<ref>Marbre de Paros, ép. 57, et Pline l'Ancien, ''Histoire naturelle'', II, 149 (DK 59 A 11) ; Plutarque, ''Vie de Lysandre'', 12 (DK 59 A 12) ; Diogène Laërce, II, 10 (DK 59 A 1). Le Marbre de Paros donne 468/7, Pline la deuxième année de la 78{{e}} Olympiade (467/6).</ref>. Plusieurs sources affirment qu'il l'avait prédite. Burnet jugeait cette prédiction absurde, et Curd rappelle que la chute d'une météorite particulière, en un lieu et à un moment donnés, ne pouvait être prévue<ref>Burnet, 1930, chap. VI ; Curd, 2007, p. 132 et n. 10.</ref>. Selon Plutarque, Anaxagore enseignait que les astres, pierres lourdes maintenues par la tension de la révolution, pouvaient tomber si ce mouvement venait à se relâcher ; Daniel Graham comprend en ce sens que la « prédiction » portait sur la chute de pierres célestes en général, et non sur un événement particulier<ref>Plutarque, ''Vie de Lysandre'', 12 (DK 59 A 12) ; Daniel W. Graham, « Anaxagoras and the Comet », ''Ancient Philosophy'', vol. 33, 2013, p. 1-18 ; Evangelos Th. Theodossiou, P. G. Niarchos, V. N. Manimanis et Wayne Orchiston, « The Fall of a Meteorite at Aegos Potami in 467/466 BC », ''Journal of Astronomical History and Heritage'', vol. 5, 2002, p. 135-140.</ref>. Plutarque rapporte aussi, d'après Daïmachos, qu'un corps enflammé fut observé pendant soixante-quinze jours avant la chute<ref>Plutarque, ''Vie de Lysandre'', 12 (DK 59 A 12) ; Curd, 2007, p. 132.</ref>. Pline dit même qu'Anaxagore avait annoncé la chute d'une pierre venue du soleil ; P. J. Bicknell a suggéré que ce récit pourrait garder le souvenir de l'observation d'une tache solaire<ref>Pline l'Ancien, ''Histoire naturelle'', II, 149 (DK 59 A 11) ; P. J. Bicknell, « Did Anaxagoras Observe a Sunspot in 467 B.C.? », ''Isis'', vol. 59, 1968, p. 87-90. L'astronome N. J. Woolf voit dans cette chute l'origine du modèle d'Anaxagore pour les phases et les éclipses de la lune, ce qui reste une hypothèse (« Anaxagoras and the Scientist/Laity Interaction », ''Vistas in Astronomy'', vol. 39, 1995, p. 699-709).</ref>. Quoi qu'il en soit, l'événement fut associé au nom d'Anaxagore et parut confirmer sa thèse selon laquelle les corps célestes sont faits de pierre.
=== Météorologie ===
Anaxagore expliquait les nuages et la neige à peu près comme Anaximène<ref>Aétius, III, 4, 2 (DK 59 A 85) ; Curd, 2007, p. 223.</ref>. Le fragment B16 décrit une série de solidifications : des nuages se sépare l'eau, de l'eau la terre, et de la terre les pierres, que le froid solidifie<ref>Anaxagore, fragment B16 (DK 59 B 16).</ref>.
La formation de la grêle posait un problème particulier : comment de la glace peut-elle se former en été ? Selon Aristote, qui critique cette théorie, Anaxagore soutenait que la grêle se forme lorsqu'un nuage est poussé vers la région supérieure, plus froide ; les chaleurs de l'été, en poussant les nuages plus haut, expliqueraient la fréquence des orages de grêle en été et dans les pays chauds<ref>Aristote, ''Météorologiques'', I, 12, 348a14-20 et 348b12-16 (DK 59 A 85), avec le commentaire d'Alexandre d'Aphrodise, 49, 13 ; Aétius, III, 4, 2 (DK 59 A 85), qui ajoute que les gouttes s'arrondissent en tombant ; Curd, 2007, p. 223.</ref>. Cette explication présente une analogie avec le rôle que la météorologie moderne attribue aux courants ascendants dans la formation de la grêle, sans qu'il faille lui prêter une théorie de la convection.
L'arc-en-ciel, selon le fragment B19, est le reflet du soleil dans les nuages ; il annonce la tempête, car l'eau répandue autour du nuage produit du vent ou se déverse en pluie<ref>Anaxagore, B19, conservé par les scholies BT à l’''Iliade'', XVII, 547 (DK 59 B 19) ; sur le texte, Friedrich Solmsen, « Anaxagoras B 19 Diels-Kranz », ''Hermes'', vol. 91, 1963, p. 251-252.</ref>.
Le tonnerre et l'éclair sont produits, selon Anaxagore, lorsque du feu venu de l'éther tombe dans les nuages : l'éclat du feu est l'éclair, et le sifflement du feu qui s'éteint est le tonnerre<ref>Aristote, ''Météorologiques'', II, 9, 369b14-19 (DK 59 A 84) ; Aétius, III, 3, 4 (DK 59 A 84) ; Sénèque, ''Questions naturelles'', II, 12, 3 et II, 19 (DK 59 A 84).</ref>. Aristote précise qu'Anaxagore tenait l'éclair pour réellement antérieur au tonnerre, les choses se passant comme elles apparaissent ; l'explication de ce décalage par la plus grande vitesse de la vue est celle d'Aristote, non celle d'Anaxagore<ref>Aristote, ''Météorologiques'', II, 9, 369b7-19.</ref>.
Les tremblements de terre sont attribués, selon Aristote, à l'éther qui, tendant naturellement vers le haut, se trouve emprisonné dans les cavités souterraines ; Aétius et Hippolyte parlent plutôt d'air qui pénètre sous la terre et l'ébranle<ref>Aristote, ''Météorologiques'', II, 7, 365a19-35 (DK 59 A 89) ; Aétius, III, 15, 4 (DK 59 A 89) ; Hippolyte, ''Réfutation'', I, 8, 12 (DK 59 A 42).</ref>.
=== Hydrologie ===
Anaxagore expliquait la crue estivale du Nil par la fonte des neiges dans les régions montagneuses d'Éthiopie<ref>Hippolyte, ''Réfutation'', I, 8, 5 (DK 59 A 42) ; Aétius, IV, 1, 3 (DK 59 A 91) ; Sénèque, ''Questions naturelles'', IV a, 2, 17 (DK 59 A 91) ; Diodore de Sicile, I, 38, 4 ; Schofield (1980, p. 34) relève qu'Eschyle adopte cette explication dans les ''Suppliantes'' (559-561).</ref>. Hérodote jugeait déjà cette explication la plus séduisante et la plus fausse, et Sénèque la rejette. Elle situe correctement l'origine des eaux dans les hauts plateaux d'Éthiopie, mais les crues sont dues aux pluies de mousson et non à la fonte des neiges<ref>Hérodote, II, 22 (DK 59 A 91) ; Curd, 2007, p. 225.</ref>. Quant à la mer, elle serait née de l'eau stagnante originelle, dont la partie la plus fine s'évapora sous l'effet du soleil, laissant un résidu salé et amer<ref>Aétius, III, 16, 2 (DK 59 A 90) ; Hippolyte, ''Réfutation'', I, 8, 4 (DK 59 A 42) ; Diogène Laërce, II, 8 (DK 59 A 1).</ref>.
=== D'autres mondes ? ===
Le fragment B4a affirme que des hommes et d'autres animaux se sont composés, qu'il existe des cités habitées et des champs cultivés « comme chez nous », un soleil, une lune et d'autres astres « comme chez nous » ; Anaxagore précise qu'il a dit cela de la séparation parce qu'elle se produirait non seulement chez nous, mais aussi ailleurs<ref>Anaxagore, B4a (DK 59 B 4a), cité par Simplicius, ''Commentaire sur la Physique'', 34, 29-35, 9, et commenté en 157, 9-24.</ref>. Le sens de cet « ailleurs » est discuté. Hermann Fränkel y voyait une expérience de pensée. Jaap Mansfeld objecte que le texte ne présente pas cet autre monde comme hypothétique ; s'appuyant sur B3 et B6, il propose d'y voir un monde infiniment petit, situé au-dessous du seuil de la perception et en tout semblable au nôtre, parce qu'il est fait des mêmes composants<ref>Jaap Mansfeld, « Anaxagoras' Other World », ''Phronesis'', vol. 25, 1980, p. 1-4, ici p. 2-3, qui discute aussi la lecture de Fränkel, reprend une idée de P. Leon (1927) et compare ces mondes emboîtés à l'image répétée à l'infini d'une boîte de chocolats Droste. Aétius (II, 1, 2 = DK 59 A 63) range d'ailleurs Anaxagore parmi les partisans d'un monde unique.</ref>. D'autres interprètes y voient des mondes séparés dans l'espace, ou des tourbillons secondaires en bordure du tourbillon principal ; John Sisko propose des mondes emboîtés vers le petit comme vers le grand<ref>Curd et Sisko, ''SEP'', 2026, § 4.3 ; Sisko, 2003, p. 87-114.</ref>.
=== Synthèse : la cosmologie naturaliste d'Anaxagore ===
La cosmologie d'Anaxagore se caractérise par trois traits.
Le premier est l'unité de la nature : les corps célestes sont faits des mêmes ingrédients que la terre, puisque ce sont des pierres arrachées à celle-ci<ref>Aétius, II, 13, 3 (DK 59 A 71) ; Hippolyte, ''Réfutation'', I, 8, 6 (DK 59 A 42) ; Gershenson et Greenberg, 1964, p. 47.</ref>. Il n'existe pas de différence de nature entre le monde sublunaire et le ciel, contrairement à ce qu'affirmera Aristote<ref>Aristote, ''Du ciel'', I, 2-3, 268b11-270b25.</ref>.
Le deuxième est l'explication physique : une fois la rotation initiée par le Noûs, les phénomènes cosmologiques et météorologiques s'expliquent par des processus tels que la rotation, la séparation selon la densité, l'évaporation ou la condensation, sans recours à des interventions divines<ref>Curd, 2007, p. 222 ; Gershenson et Greenberg, 1964, p. 26.</ref>.
Le troisième est l'usage de l'analogie : Anaxagore explique les phénomènes célestes par analogie avec des phénomènes terrestres observables<ref>Gershenson et Greenberg, 1964, p. 24 et 34.</ref>.
Ces traits donnent à la cosmologie d'Anaxagore un caractère naturaliste marqué, qui représente une étape importante dans l'histoire de l'explication physique de la nature. Il ne faut pas pour autant voir en lui le « fondateur de la méthode scientifique », selon une expression que l'on rencontre parfois ; mieux vaut parler d'un jalon, où se combinent intuitions fécondes et erreurs caractéristiques de son temps<ref>La formule forte est celle de Gershenson et Greenberg (1964, préface), qui font d'Anaxagore le premier savant au sens moderne. Pour une perspective plus prudente, voir G. E. R. Lloyd, ''Early Greek Science: Thales to Aristotle'', Londres, Chatto & Windus, 1970.</ref>.
== La physiologie et la biologie ==
Bien qu'Anaxagore soit surtout connu pour sa cosmologie, les témoignages indiquent qu'il s'intéressa aussi à la nutrition, à la perception, à l'embryologie et aux êtres vivants<ref>Curd, 2007, p. 225-229 ; Gershenson et Greenberg, 1964, p. 55-57 ; Hubert Erhard, « Anaxagoras als Biologe », ''Sudhoffs Archiv für Geschichte der Medizin und der Naturwissenschaften'', vol. 35, 1942, p. 117-140.</ref>.
=== La théorie de la nutrition ===
L'une des questions biologiques qu'Anaxagore cherche à élucider est celle de la nutrition : comment une nourriture simple, comme le pain et l'eau, peut-elle nourrir les cheveux, les veines, les artères, la chair, les nerfs et les os ?<ref>Aétius, I, 3, 5 (DK 59 A 46) ; Simplicius, ''Commentaire sur la Physique'', 460, 4-20 (DK 59 A 45) ; Platon, ''Phédon'', 96c-d (DK 59 A 46) ; Lucrèce, ''De la nature'', I, 830-920 (DK 59 A 44).</ref> Cette observation pose un problème au regard de l'interdit parménidien : une substance nouvelle, la chair, semble naître de ce qui n'est pas chair.
La réponse d'Anaxagore est cohérente avec sa métaphysique : le pain doit déjà contenir de la chair, du sang, des os et toutes les autres substances corporelles, en proportions si faibles qu'elles restent imperceptibles<ref>Simplicius, ''Commentaire sur la Physique'', 460, 4-20 (DK 59 A 45), qui ajoute que, si les arbres se nourrissent d'eau, l'eau doit contenir du bois, de l'écorce et des fruits.</ref>. Lorsque nous mangeons, la chair contenue dans la nourriture vient s'ajouter à la chair du corps<ref>Aristote, ''Génération des animaux'', I, 18, 723a10-11.</ref>.
Cette théorie soulève une autre difficulté : comment le corps dirige-t-il la chair vers les muscles et les parties osseuses vers les os ? Les fragments ne le disent pas. Certains interprètes supposent qu'Anaxagore rattachait cette fonction au Noûs présent dans les êtres vivants<ref>Gershenson et Greenberg, 1964, p. 24, pour qui la digestion est, chez Anaxagore, une fonction de l'intellect ; Curd (2007, p. 175-177) suggère que les semences contiennent, par le Noûs ou l'âme, un principe d'organisation, tout en qualifiant cette reconstruction de spéculative.</ref>.
=== La théorie de la perception ===
Anaxagore élabora une théorie de la perception fondée sur le principe que le semblable n'est pas affecté par le semblable : la perception se fait par les contraires<ref>Théophraste, ''Du sens'', 27 (DK 59 A 92).</ref>. Il s'opposait ainsi à Empédocle, pour qui la perception se fait par le semblable<ref>Théophraste, ''Du sens'', 1-2 (DK 31 A 86).</ref>.
Pour qu'une perception ait lieu, il doit exister une différence entre l'organe et l'objet perçu : ce qui est exactement aussi chaud ou aussi froid que nous ne nous réchauffe ni ne nous refroidit à son contact<ref>Théophraste, ''Du sens'', 28 (DK 59 A 92).</ref>. La vision se fait par le reflet de l'objet dans la pupille, qui ne se produit que sur une couleur différente<ref>Théophraste, ''Du sens'', 27 (DK 59 A 92) ; Gershenson et Greenberg, 1964, p. 28-32.</ref>.
Anaxagore en concluait que toute perception s'accompagne de douleur ou de gêne (λύπη), car tout contact du dissemblable produit une irritation, sensible lorsque la sensation est trop longue ou trop intense<ref>Théophraste, ''Du sens'', 29 (DK 59 A 92) ; Aétius, IV, 9, 16, et Aristote, ''Éthique à Nicomaque'', VII, 15, 1154b7-9 (DK 59 A 94) ; James Warren, « Anaxagoras on Perception, Pleasure, and Pain », ''Oxford Studies in Ancient Philosophy'', vol. 33, 2007, p. 19-54 ; Curd, 2007, p. 169 et n. 37.</ref>.
=== La reproduction et l'embryologie ===
Les témoignages sur l'embryologie d'Anaxagore ne concordent pas entièrement. Selon Aristote, il comptait parmi ceux pour qui la différence des sexes est déjà présente dans la semence : la semence vient du mâle, la femelle ne fournissant que le lieu, et les mâles proviennent du côté droit, les femelles du côté gauche<ref>Aristote, ''Génération des animaux'', IV, 1, 763b30-33 (DK 59 A 107) ; Hippolyte, ''Réfutation'', I, 8, 12 (DK 59 A 42). Schofield (1980, p. 34) relève que l'idée selon laquelle la mère ne fournit qu'un lieu apparaît dans les ''Euménides'' d'Eschyle (657-666).</ref>. D'autres sources lui prêtent l'idée d'une contribution séminale des deux parents, l'enfant ressemblant à celui qui a fourni le plus de semence<ref>Censorinus, ''Du jour natal'', 5, 2-4 et 6, 6-8 (DK 59 A 107 et A 111) ; Aétius, V, 7, 4 (DK 59 A 111), qui associe Anaxagore et Parménide ; Curd, 2007, p. 174 et 226-227.</ref>. Owen Kember juge le témoignage d'Aristote inexact sur plusieurs points et reconstruit ainsi la théorie : les deux parents émettraient une semence venue de tout le corps, la part de chacun déterminerait la ressemblance, et le sexe dépendrait du côté de l'utérus où se dépose le mélange<ref>Owen Kember, « Anaxagoras' Theory of Sex Differentiation and Heredity », ''Phronesis'', vol. 18, 1973, p. 1-14, ici p. 1 et 14, contre l'interprétation reçue (Blersch, Erhard, Lesky, Lloyd), fondée sur Aristote.</ref>.
=== La génération des animaux et des plantes ===
Selon les témoignages, Anaxagore distinguait la première génération des animaux et leur reproduction ultérieure : les animaux naquirent d'abord de l'humide, du chaud et du terreux, puis les uns des autres<ref>Diogène Laërce, II, 9 (DK 59 A 1) ; Hippolyte, ''Réfutation'', I, 8, 12 (DK 59 A 42).</ref>. Irénée lui attribue l'idée que les animaux sont issus de semences tombées du ciel, et Théophraste celle que l'air contient les semences des plantes, entraînées vers le sol par la pluie<ref>Irénée, ''Contre les hérésies'', II, 14, 2 (DK 59 A 113) ; Théophraste, ''Recherches sur les plantes'', III, 1, 4 (DK 59 A 117) ; Schofield, 1980, p. 125.</ref>.
Anaxagore aurait tenu les plantes pour des animaux attachés au sol<ref>Plutarque, ''Questions naturelles'', 1, 911d (DK 59 A 116).</ref>. Le traité pseudo-aristotélicien ''Sur les plantes'' lui prête l'idée qu'elles éprouvent désir, plaisir et peine, en invoquant la chute de leurs feuilles, et qu'elles possèdent intellect et connaissance<ref>Pseudo-Aristote, ''Sur les plantes'', I, 1, 815a15-20 et 815b16-17 (DK 59 A 117). Schofield (1980, p. 148, n. 40-41) juge difficile d'accorder crédit aux sentiments prêtés aux plantes, mais admet que l'attribution d'une âme et d'un intellect puisse reposer sur une source fiable.</ref>.
=== Observations biologiques diverses ===
Les sources conservent quelques observations isolées. Aristote rapporte qu'Anaxagore et d'autres physiciens prétendaient que le corbeau et l'ibis s'accouplent par le bec et que la belette met bas par la gueule, et il leur reproche d'en parler trop superficiellement et sans examen<ref>Aristote, ''Génération des animaux'', III, 6, 756b13-17 (DK 59 A 114).</ref>. L'erreur sur la belette pourrait venir de l'observation de femelles transportant leurs petits dans la gueule aussitôt après la mise bas<ref>Gershenson et Greenberg, 1964, p. 57.</ref>. Ces erreurs montrent les limites de l'observation dans la biologie d'Anaxagore. Il expliquait aussi que ce qu'on appelle proverbialement « lait d'oiseau » n'est autre que le blanc de l'œuf<ref>Anaxagore, B22, cité par Athénée, II, 57d (DK 59 B 22) ; sur le texte, Tad Brennan, « The Text of Anaxagoras Fragment DK 59 B22 », ''American Journal of Philology'', vol. 116, 1995, p. 533-537, qui y voit une explication par réduction comparable à celle de l'arc-en-ciel chez Xénophane.</ref>
=== L'homme et les animaux ===
Selon Anaxagore, l'homme est le plus intelligent des animaux parce qu'il a des mains ; Aristote renverse la formule et soutient que l'homme a des mains parce qu'il est le plus intelligent<ref>Aristote, ''Parties des animaux'', IV, 10, 687a7-10 (DK 59 A 102) ; Schofield, 1980, p. 16.</ref>. Burnet en tirait l'idée que, l'intellect étant partout le même, les différences d'intelligence entre les êtres vivants dépendent de la structure de leur corps<ref>Burnet, 1930, chap. VI.</ref>.
== L'influence et la postérité ==
Anaxagore occupe une place importante dans l'histoire de la philosophie antique. Premier philosophe de renom établi à Athènes, il y introduit la tradition ionienne de recherche sur la nature<ref>Curd, 2007, p. 129 et 142-146 ; Curd et Sisko, ''SEP'', 2026, § 6.</ref>. Les lignes qui suivent décrivent son influence avec la prudence nécessaire : il est facile, en traitant des présocratiques, de céder à la tentation des grandes filiations, alors que la documentation invite souvent à plus de retenue.
=== La transmission immédiate : Archélaos, Diogène d'Apollonie et le papyrus de Derveni ===
Le premier relais de l'influence d'Anaxagore fut son disciple Archélaos d'Athènes, qu'une partie de la tradition présente comme le maître de Socrate<ref>Diogène Laërce, II, 16 (DK 60 A 1) ; Théophraste, dans Simplicius (DK 60 A 5) ; Curd, 2007, p. 134 et n. 18. Ion de Chios rapporte que le jeune Socrate voyagea à Samos avec Archélaos (DK 60 A 3).</ref>. Clément d'Alexandrie en fait le successeur d'Anaxagore, et Eusèbe précise qu'il lui succéda à la tête de l'école de Lampsaque, indication difficile à concilier avec les témoignages qui situent son enseignement à Athènes<ref>Clément d'Alexandrie, ''Stromates'', I, 63, et Eusèbe, ''Préparation évangélique'', X, 14, 13 (DK 59 A 7) ; Curd, 2007, p. 134, n. 18.</ref>. Archélaos semble avoir prolongé la cosmologie de son maître par un récit des origines des institutions sociales, orientation qui a pu préparer le tournant moral pris ensuite par Socrate<ref>Gábor Betegh, « Archelaus on Cosmogony and the Origins of Social Institutions », ''Oxford Studies in Ancient Philosophy'', vol. 51, 2016, p. 1-40.</ref>.
Diogène d'Apollonie, dans la génération suivante, subit l'influence d'Anaxagore dans sa doctrine comme dans son style ; il fait de l'intelligence un principe cosmique, mais l'attribue à l'air et prend soin de l'appuyer sur des arguments, ce qu'Anaxagore ne faisait guère<ref>Schofield, 1980, p. 5-6 ; Curd, « Presocratic Philosophy », ''SEP'', 2020, § 9.</ref>. L'auteur du papyrus de Derveni, commentaire allégorique d'un poème orphique découvert en 1962, semble avoir repris, en les modifiant, la doctrine d'un Intellect qui gouverne le cosmos et certains aspects de la théorie des ingrédients<ref>Curd, 2007, p. 143 et n. 35 ; Gábor Betegh, ''The Derveni Papyrus: Cosmology, Theology and Interpretation'', Cambridge, Cambridge University Press, 2004, chap. 7.</ref>.
=== L'influence sur Socrate : espoirs et déceptions ===
La relation entre Anaxagore et Socrate passe, dans nos sources, par la lecture du livre. Dans le ''Phédon'', Socrate raconte sa rencontre avec la pensée d'Anaxagore, qui suscita d'abord son enthousiasme, puis sa déception<ref>Platon, ''Phédon'', 97b-98c (DK 59 A 47).</ref>. Comme on l'a vu, cette déception porte sur une attente proprement socratique, celle d'une explication par le meilleur, qu'Anaxagore n'avait pas formulée dans les termes que Socrate aurait voulus.
Le ''Phédon'' présente ensuite le passage de Socrate à une autre méthode de recherche des causes, fondée sur les Formes, que Socrate lui-même présente comme une « seconde navigation »<ref>Platon, ''Phédon'', 99c-100a ; David Sedley, « Teleology and Myth in the ''Phaedo'' », ''Proceedings of the Boston Area Colloquium in Ancient Philosophy'', vol. 5, 1989, p. 359-383 ; Curd, 2007, p. 136, n. 20, et p. 144-145.</ref>. L'idée qu'un principe intelligent gouverne l'univers, que Socrate trouve chez Anaxagore et juge insuffisamment exploitée, a pu orienter sa recherche sans qu'il faille faire d'Anaxagore sa cause nécessaire.
Dans l’''Apologie'', l'accusation portée contre Socrate inclut des thèses cosmologiques qui sont en réalité celles d'Anaxagore, ce qui montre l'association des deux penseurs dans l'esprit des Athéniens, indépendamment de la distance qui les sépare<ref>Platon, ''Apologie de Socrate'', 26d-e (DK 59 A 35) ; Curd, 2007, p. 142-143.</ref>. Xénophon présente de son côté Socrate mettant ses disciples en garde contre l'étude des phénomènes célestes à la manière d'Anaxagore, qu'il accuse d'avoir perdu la raison en identifiant le soleil au feu<ref>Xénophon, ''Mémorables'', IV, 7, 6-7 (DK 59 A 73).</ref>.
=== L'appropriation platonicienne : du Noûs au Démiurge ===
Platon reprend la doctrine du Noûs et la transforme en une cosmologie téléologique. Dans le ''Timée'', le Démiurge, artisan divin qui façonne le monde sensible en contemplant les Formes, peut se lire en dialogue avec le Noûs d'Anaxagore<ref>Platon, ''Timée'', 29a-30c et 47e-48a ; Curd, 2007, p. 145 ; Schofield, 1980, p. 61 ; Glenn R. Morrow, « Necessity and Persuasion in Plato's ''Timaeus'' », ''Philosophical Review'', vol. 59, 1950, p. 147-163 ; Luc Brisson, ''Le Même et l'Autre dans la structure ontologique du Timée de Platon'', Paris, Klincksieck, 1974.</ref>. La téléologie platonicienne va cependant bien au-delà de ce qu'affirmait Anaxagore : le Démiurge façonne le monde en vue du meilleur en contemplant des réalités intelligibles, arrière-plan qui n'a pas d'équivalent dans les fragments. Schofield note toutefois un point commun : pour Anaxagore comme pour Platon, c'est l'ordre du monde qui fait problème, non l'existence des choses, et la matière est présupposée<ref>Schofield, 1980, p. 61.</ref>.
Dans le ''Philèbe'', Socrate rappelle l'accord des sages pour dire que l'Intellect est roi du ciel et de la terre<ref>Platon, ''Philèbe'', 28c ; cf. ''Cratyle'', 413c (DK 59 A 55), où la justice est identifiée à l'Intellect « selon Anaxagore ».</ref>. Dans les ''Lois'', Platon évoque ceux qui ont osé dire que c'est l'Intellect qui a ordonné tout ce qui est dans le ciel, allusion probable à Anaxagore<ref>Platon, ''Lois'', XII, 967b-c.</ref>. Curd montre en outre que Platon réemploie le vocabulaire anaxagoréen : les Formes sont, comme le Noûs, « elles-mêmes par elles-mêmes », et les choses sensibles « participent » des Formes comme, chez Anaxagore, les choses ont part à toutes choses<ref>Curd, 2007, p. 50 et 143-145.</ref>. L'influence d'Anaxagore sur Platon est donc réelle, mais elle passe par une transformation profonde : le Noûs, principe moteur et cognitif, devient chez Platon un Intellect qui façonne le monde selon le meilleur ordre possible.
=== La critique aristotélicienne ===
Aristote, tout en reconnaissant l'importance historique d'Anaxagore, en fut l'un des critiques les plus sévères. Dans la ''Métaphysique'', il dit qu'Anaxagore parut comme un homme sobre au milieu de prédécesseurs qui parlaient au hasard<ref>Aristote, ''Métaphysique'', A, 3, 984b15-20 (DK 59 A 58).</ref>, avant de lui reprocher de se servir de l'Intellect comme d'un ''deus ex machina''<ref>Aristote, ''Métaphysique'', A, 4, 985a18-21 (DK 59 A 47).</ref>. Ses critiques portent aussi, dans le traité ''De l'âme'', sur la confusion entre l'âme et l'intellect et sur la manière dont le Noûs connaît<ref>Aristote, ''De l'âme'', I, 2 et III, 4 (DK 59 A 55, A 99, A 100) ; Curd, 2007, p. 146.</ref>.
Aristote décrit par ailleurs la doctrine d'Anaxagore au moyen de son propre vocabulaire, notamment le terme d'homéomère (ὁμοιομερής), qu'Anaxagore n'a probablement jamais employé<ref>Curd, 2007, p. 147-150 ; Schofield, 1980, p. 128-132.</ref>. La distinction entre parties homéomères (chair, os, sang) et anhoméomères (main, visage) appartient à la biologie d'Aristote lui-même ; il s'en sert pour décrire rétrospectivement la doctrine d'Anaxagore<ref>Aristote, ''Parties des animaux'', II, 1, 646a12-24 ; ''Génération et corruption'', I, 1, 314a18-20 (DK 59 A 46).</ref>.
=== L'héritage dans la philosophie hellénistique et tardive ===
Après Aristote, la pensée d'Anaxagore continua d'exercer une influence diffuse. La doctrine stoïcienne d'une raison divine immanente qui gouverne la nature présente des analogies avec le Noûs, mais les Stoïciens, matérialistes, rejettent la séparation de l'intellect et des corps, et aucune dépendance directe n'est établie<ref>A. A. Long et D. N. Sedley, ''The Hellenistic Philosophers'', vol. I, Cambridge, Cambridge University Press, 1987 ; Michael J. White, « Stoic Natural Philosophy (Physics and Cosmology) », dans Brad Inwood (éd.), ''The Cambridge Companion to the Stoics'', Cambridge, Cambridge University Press, 2003, p. 124-152.</ref>.
Du côté épicurien, Lucrèce critique longuement l’''homoeomeria'' d'Anaxagore<ref>Lucrèce, ''De la nature'', I, 830-920 (DK 59 A 44) ; David Sedley, ''Lucretius and the Transformation of Greek Wisdom'', Cambridge, Cambridge University Press, 1998.</ref>, et Cicéron fait railler par l'épicurien Velléius son Intellect illimité<ref>Cicéron, ''De la nature des dieux'', I, 11, 26 (DK 59 A 48).</ref>. Selon Dioclès, cité par Diogène Laërce, Épicure approuvait pourtant Anaxagore plus que tout autre ancien, tout en s'en écartant sur certains points<ref>Diogène Laërce, X, 12 (DK 59 A 26).</ref>.
Dans la tradition néoplatonicienne, Anaxagore est lu à travers Platon et Aristote ; les commentateurs formulent ses principes du « tout en tout » et de la prédominance dans leur propre vocabulaire<ref>Proclus, ''Éléments de théologie'', 103 ; Schofield, 1980, p. 155, n. 52.</ref>. Simplicius, au VI{{e}} siècle, consacre de longs passages de son commentaire sur la ''Physique'' d'Aristote à Anaxagore : c'est à lui que nous devons la plupart des fragments<ref>Simplicius, ''Commentaire sur la Physique'', passim ; Teodorsson, 1982, p. 10.</ref>.
=== Échos littéraires ===
À l'époque moderne, Goethe met en scène Anaxagore face à Thalès dans la « Nuit de Walpurgis classique » du second ''Faust'' (acte II). Leur dispute sur la formation des roches et des montagnes, par le feu ou par l'eau, a longtemps été lue comme une parodie de la querelle du XVIII{{e}} siècle entre vulcanistes et neptunistes ; Kenneth Weisinger juge cette lecture insuffisante<ref>Kenneth D. Weisinger, « A Note on Homunculus, Thales, and Anaxagoras », ''Monatshefte'', vol. 64, 1972, p. 237-246, ici p. 237.</ref>.
=== Un héritage à ne pas surinterpréter ===
Il est tentant de présenter Anaxagore comme un précurseur de la science mécaniste moderne, de l'atomisme ou de la physique contemporaine. Il faut résister à cette tentation. Au XVII{{e}} siècle, certains philosophes mécanistes ont invoqué les présocratiques, mais c'est l'atomisme de Leucippe et de Démocrite, transmis par Épicure et Lucrèce, qui a nourri la physique corpusculaire de Gassendi, de Boyle ou de Newton. L'influence propre d'Anaxagore sur cette tradition reste limitée et indirecte : sa doctrine de la divisibilité illimitée et du mélange universel s'oppose aux postulats de l'atomisme<ref>Alan Chalmers, « Atomism from the 17th to the 20th Century », ''Stanford Encyclopedia of Philosophy'', 2005 (révisions ultérieures) ; Andrew Pyle, ''Atomism and its Critics: From Democritus to Newton'', Bristol, Thoemmes Press, 1995.</ref>.
On rencontre aussi des rapprochements entre le « tout dans tout » et certaines idées de la physique moderne (théorie des champs, intrication quantique, non-séparabilité). Ces rapprochements, parfois suggérés par des physiciens eux-mêmes, peuvent avoir une valeur pédagogique ; ils ne doivent pas être transformés en affirmations de continuité doctrinale. La physique quantique procède d'un appareil mathématique et expérimental sans équivalent chez les Grecs, et les analogies qu'on peut tracer avec Anaxagore relèvent de la métaphore rétrospective<ref>Werner Heisenberg, ''Physics and Philosophy'', New York, Harper, 1958, évoque les présocratiques sans établir de filiation précise avec la physique quantique.</ref>.
On a parfois suggéré que les théologiens médiévaux auraient vu dans le Noûs un précurseur du Dieu créateur. La prudence s'impose : Thomas d'Aquin, Maïmonide et Avicenne s'inscrivent dans une tradition aristotélicienne et néoplatonicienne déjà élaborée, où le Noûs d'Anaxagore n'apparaît, quand il est mentionné, qu'à travers Aristote, et le plus souvent pour être critiqué. Le Noûs, qui ordonne un mélange qu'il ne crée pas, ne se confond d'ailleurs pas avec un Dieu créateur<ref>Schofield, 1980, p. 61 ; Curd, 2007, p. 61 et 201, n. 17 ; Étienne Gilson, ''L'Esprit de la philosophie médiévale'', Paris, Vrin, 1932 ; Cristina D'Ancona, « Greek into Arabic: Neoplatonism in Translation », dans Peter Adamson et Richard C. Taylor (éd.), ''The Cambridge Companion to Arabic Philosophy'', Cambridge, Cambridge University Press, 2005, p. 10-31.</ref>.
Une dernière mise en garde concerne la prédiction d'éclipses et la chute de la météorite d'Aigos Potamos, parfois présentées comme des triomphes d'une « méthode scientifique » naissante. Les sources qui attribuent à Anaxagore ces prédictions sont tardives et souvent légendaires ; les historiens se montrent prudents, voire sceptiques<ref>Curd, 2007, p. 132 et n. 9-10 ; Burnet, 1930, chap. VI ; Graham, « Anaxagoras and the Comet », 2013.</ref>. L'importance d'Anaxagore ne tient pas à la précision de ses prédictions, mais à la cohérence et à l'ambition explicative de son système naturaliste, qui rend compte des phénomènes célestes par des principes physiques accessibles à la raison.
=== Bilan ===
La réception d'Anaxagore est marquée par un contraste. D'un côté, Aristote salue l'introduction du Noûs comme principe cosmique et loue sa sobriété ; de l'autre, Socrate, Platon et Aristote jugent insuffisant l'usage qu'il en fait. Anaxagore apparaît ainsi à la fois comme le penseur qui a rendu pensable une cause intellectuelle du cosmos et comme celui qui, au goût de ses successeurs, l'a laissée inemployée<ref>Curd, 2007, p. 142-146 et 204-205 ; Schofield, 1980, p. 59-61 ; Graham, ''Science before Socrates'', 2013.</ref>.
Cette limite même a été féconde. Socrate se tourne vers la recherche de causes que le Noûs anaxagoréen ne fournissait pas. Platon développe, dans le ''Timée'', une cosmologie téléologique où le Démiurge façonne le monde en contemplant les Formes. Aristote élabore, contre Platon autant que contre Anaxagore, une doctrine des causes qui intègre la cause finale. Tous trois ont puisé chez Anaxagore, au prix d'une transformation qui leur appartient ; leurs doctrines ont leurs propres fondements, et se construisent souvent en réaction à Anaxagore autant qu'en prolongement de ses thèses.
Sur le plan scientifique, l'héritage d'Anaxagore est surtout celui d'un modèle d'explication naturaliste. Le soleil n'est plus Hélios mais une pierre incandescente ; la lune n'est plus une déesse mais un corps terreux qui reçoit sa lumière du soleil ; la foudre n'est plus le trait de Zeus mais un phénomène atmosphérique. Cet héritage a nourri durablement la tradition philosophique et scientifique grecque. Il serait pourtant excessif de lui attribuer la fondation d'une « méthode scientifique » au sens moderne : celle-ci suppose la preuve démonstrative, la mathématisation de la nature et bien d'autres étapes que la pensée d'Anaxagore n'a pas franchies<ref>Gershenson et Greenberg (1964) défendent une version forte de la thèse d'Anaxagore fondateur de la méthode scientifique, à nuancer ; cf. Lloyd, 1970.</ref>.
Anaxagore occupe en définitive une place singulière : celle d'un pionnier qui ouvre une voie sans la parcourir entièrement, et qui lègue à ses successeurs à la fois une doctrine et la tâche de penser ce qu'elle laisse ouvert. Placé entre la cosmologie ionienne, dont il est le dernier grand représentant à Athènes, et la philosophie classique, qu'il contribue à préparer, il mérite d'être étudié pour lui-même, et non seulement à travers les influences qu'il a exercées.
== Notes et références ==
{{references|colonnes=2}}
== Bibliographie ==
=== Textes anciens : éditions et traductions ===
{| class="wikitable"
! Auteur ou éditeur !! Référence !! Collection (n°)
|-
| Diels, Hermann ; Kranz, Walther (éd.) || ''Die Fragmente der Vorsokratiker'', 6{{e}} éd., Berlin, Weidmann, 1951-1952, 3 vol. (Anaxagore : vol. II, p. 5-44, chap. 59, numérotation DK) ||
|-
| Laks, André ; Most, Glenn W. (éd.) || ''Early Greek Philosophy'', Cambridge (Mass.), Harvard University Press, 2016, 9 vol. (Anaxagore : chap. 25, numérotation LM) || Loeb Classical Library
|-
| Laks, André ; Most, Glenn W. (éd.) || ''Les Débuts de la philosophie. Des premiers penseurs grecs à Socrate'', Paris, Fayard, 2016 ||
|-
| Curd, Patricia (éd.) || ''Anaxagoras of Clazomenae: Fragments and Testimonia. A Text and Translation with Notes and Essays'', Toronto, University of Toronto Press, 2007 || Phoenix Presocratics, 6 ; Phoenix Supplementary Volume, 44
|-
| Sider, David (éd.) || ''The Fragments of Anaxagoras, Edited with an Introduction and Commentary'', Meisenheim am Glan, Hain, 1981 ; 2{{e}} éd. revue, Sankt Augustin, Academia Verlag, 2005 || Beiträge zur klassischen Philologie, 118 (1{{re}} éd.) ; International Pre-Platonic Studies (2{{e}} éd.)
|-
| Zafiropulo, Jean || ''Anaxagore de Clazomène'', I : ''Le Mythe grec traditionnel de Thalès à Platon'' ; II : ''Théorie et fragments'', Paris, Les Belles Lettres, 1948 || Collection d'études anciennes
|-
| Kirk, G. S. ; Raven, J. E. ; Schofield, Malcolm || ''The Presocratic Philosophers: A Critical History with a Selection of Texts'', 2{{e}} éd., Cambridge, Cambridge University Press, 1983 ||
|-
| Simplicius || ''In Aristotelis Physicorum libros commentaria'', éd. Hermann Diels, Berlin, Reimer, 1882-1895 || Commentaria in Aristotelem Graeca, 9-10
|-
| Platon || ''Phédon'', trad. Monique Dixsaut, Paris, Flammarion, 1991 || GF Flammarion
|-
| Platon || ''Apologie de Socrate. Criton'', trad. Luc Brisson, Paris, Flammarion, 1997 || GF Flammarion
|-
| Platon || ''Timée. Critias'', trad. Luc Brisson, Paris, Flammarion, 1992 || GF Flammarion
|-
| Platon || ''Philèbe'', texte établi et traduit par Auguste Diès, Paris, Les Belles Lettres, 1941 || Collection des universités de France
|-
| Platon || ''Les Lois'', texte établi et traduit par Édouard des Places (livres I-VI, 1951) et Auguste Diès (livres VII-XII, 1956), Paris, Les Belles Lettres || Collection des universités de France
|-
| Aristote || ''La Métaphysique'', trad. Jean Tricot, Paris, Vrin, 1933 (nouvelle éd. 1953) ||
|-
| Aristote || ''Physique'', trad. Pierre Pellegrin, Paris, Flammarion, 2000 || GF Flammarion
|-
| Aristote || ''Traité du ciel'', trad. Jean Tricot, Paris, Vrin, 1949 ||
|-
| Aristote || ''De la génération et de la corruption'', trad. Jean Tricot, Paris, Vrin, 1934 ||
|-
| Aristote || ''Les Météorologiques'', trad. Jean Tricot, Paris, Vrin, 1941 ||
|-
| Aristote || ''De la génération des animaux'', texte établi et traduit par Pierre Louis, Paris, Les Belles Lettres, 1961 || Collection des universités de France
|-
| Aristote || ''Les Parties des animaux'', texte établi et traduit par Pierre Louis, Paris, Les Belles Lettres, 1956 || Collection des universités de France
|-
| Aristote || ''Histoire des animaux'', trad. Jean Tricot, Paris, Vrin, 1957 ||
|-
| Aristote || ''De l'âme'', trad. Richard Bodéüs, Paris, Flammarion, 1993 || GF Flammarion
|-
| Diogène Laërce || ''Vies et doctrines des philosophes illustres'', sous la dir. de Marie-Odile Goulet-Cazé, Paris, Le Livre de Poche, 1999 || La Pochothèque
|-
| Théophraste || ''De sensibus'', éd. et trad. angl. dans George M. Stratton, ''Theophrastus and the Greek Physiological Psychology before Aristotle'', Londres, Allen & Unwin, 1917 ||
|-
| Lucrèce || ''De la nature'', texte établi et traduit par Alfred Ernout, Paris, Les Belles Lettres, 1920 || Collection des universités de France
|-
| Cicéron || ''La Nature des dieux'', trad. Clara Auvray-Assayas, Paris, Les Belles Lettres, 2002 || La Roue à livres
|-
| Plutarque || ''Vies parallèles'', trad. Anne-Marie Ozanam, éd. François Hartog, Paris, Gallimard, 2001 || Quarto
|-
| Xénophon || ''Mémorables'', texte établi et traduit par Louis-André Dorion et Michele Bandini, Paris, Les Belles Lettres, 2000-2011 || Collection des universités de France
|}
=== Études sur Anaxagore ===
{| class="wikitable"
! Auteur !! Référence !! Collection (n°)
|-
| Althoff, Jochen || « Presocratic Discourse in Poetry and Prose: The Case of Empedocles and Anaxagoras », ''Studies in History and Philosophy of Science'', vol. 43, 2012, p. 293-299 ||
|-
| Arsenijević, Miloš ; Popović, Saša ; Vuletić, Miloš || « Anaxagoras, the Thoroughgoing Infinitist: The Relation between his Teachings on Multitude and on Heterogeneity », ''European Journal of Analytic Philosophy'', vol. 15, 2019, p. 35-70 ||
|-
| Bargrave-Weaver, D. || « The Cosmogony of Anaxagoras », ''Phronesis'', vol. 4, 1959, p. 77-91 ||
|-
| Barnes, Jonathan || ''The Presocratic Philosophers'', Londres, Routledge, 1979, 2 vol. ; éd. révisée en un volume, 1982 ||
|-
| Betegh, Gábor || « Archelaus on Cosmogony and the Origins of Social Institutions », ''Oxford Studies in Ancient Philosophy'', vol. 51, 2016, p. 1-40 ||
|-
| Bicknell, P. J. || « Did Anaxagoras Observe a Sunspot in 467 B.C.? », ''Isis'', vol. 59, 1968, p. 87-90 ||
|-
| Brennan, Tad || « The Text of Anaxagoras Fragment DK 59 B22 », ''American Journal of Philology'', vol. 116, 1995, p. 533-537 ||
|-
| Bröcker, Walter || « Die Lehre des Anaxagoras », ''Kant-Studien'', vol. 42, 1942-1943, p. 176-189 ||
|-
| Burnet, John || ''Early Greek Philosophy'', 4{{e}} éd., Londres, A. & C. Black, 1930 (le chapitre VI reprend celui de la 3{{e}} éd., 1920) ||
|-
| Cleve, Felix M. || ''The Philosophy of Anaxagoras: An Attempt at Reconstruction'', New York, King's Crown Press, 1949 ; nouvelle éd., ''The Philosophy of Anaxagoras, as Reconstructed by Felix M. Cleve'', La Haye, Martinus Nijhoff, 1973 ||
|-
| Cornford, F. M. || « Anaxagoras' Theory of Matter », ''Classical Quarterly'', vol. 24, 1930, p. 14-30 et 83-95 ||
|-
| Couprie, Dirk L. || « Anaxagoras on the Milky Way and Lunar Eclipses », ''Hyperboreus'', vol. 23, 2017, p. 181-207 ||
|-
| Couprie, Dirk L. || « Anaxagoras on the Light and Phases of the Moon », ''Hyperboreus'', vol. 24, 2018, p. 12-39 ||
|-
| Curd, Patricia || ''The Legacy of Parmenides: Eleatic Monism and Later Presocratic Thought'', Princeton, Princeton University Press, 1998 ; rééd. Las Vegas, Parmenides Publishing, 2004 ||
|-
| Curd, Patricia ; Sisko, John || « Anaxagoras », ''Stanford Encyclopedia of Philosophy'', 1{{re}} publication 2007, révision substantielle 2026 ||
|-
| Davison, J. A. || « Protagoras, Democritus, and Anaxagoras », ''Classical Quarterly'', n.s. vol. 3, 1953, p. 33-45 ||
|-
| DeFilippo, Joseph G. || « Reply to André Laks on Anaxagoras' ΝΟΥΣ », ''Southern Journal of Philosophy'', vol. 31, suppl., 1993, p. 39-48 ||
|-
| Deichgräber, Karl || « Hymnische Elemente in der philosophischen Prosa der Vorsokratiker », ''Philologus'', vol. 88, 1933, p. 347-361 ||
|-
| Dover, K. J. || « The Freedom of the Intellectual in Greek Society », ''Talanta'', vol. 7, 1975, p. 24-54 ||
|-
| Drozdek, Adam || « Anaxagoras and the Everything in Everything Principle », ''Hermes'', vol. 133, 2005, p. 163-177 ||
|-
| Erhard, Hubert || « Anaxagoras als Biologe », ''Sudhoffs Archiv für Geschichte der Medizin und der Naturwissenschaften'', vol. 35, 1942, p. 117-140 ||
|-
| Freeman, Kathleen || « Anaxagoras », ''Greece & Rome'', vol. 4, 1935, p. 65-75 ||
|-
| Fritz, Kurt von || « Der ΝΟΥΣ des Anaxagoras », ''Archiv für Begriffsgeschichte'', vol. 9, 1964, p. 87-102 ; repris dans ''Grundprobleme der Geschichte der antiken Wissenschaft'', Berlin et New York, De Gruyter, 1971, p. 576-593 ||
|-
| Furley, David J. || « Anaxagoras in Response to Parmenides », ''Canadian Journal of Philosophy'', suppl. vol. 2, 1976, p. 61-85 ; repris dans ''Cosmic Problems'', Cambridge, Cambridge University Press, 1989, p. 47-65 ||
|-
| Furley, David J. || « Anaxagoras, Plato and the Naming of Parts », dans Victor Caston et Daniel W. Graham (éd.), ''Presocratic Philosophy: Essays in Honour of Alexander Mourelatos'', Aldershot, Ashgate, 2002, p. 119-126 ||
|-
| Furth, Montgomery || « A “Philosophical Hero”? Anaxagoras and the Eleatics », ''Oxford Studies in Ancient Philosophy'', vol. 9, 1991, p. 95-129 ||
|-
| Geffcken, Johannes || « Die Asebeia des Anaxagoras », ''Hermes'', vol. 42, 1907, p. 127-133 ||
|-
| Gershenson, Daniel E. ; Greenberg, Daniel A. || ''Anaxagoras and the Birth of Scientific Method'', New York et Londres, Blaisdell, 1964, version abrégée de la première partie de l'ouvrage suivant (à utiliser avec précaution : la thèse d'une « naissance de la méthode scientifique » doit être nuancée) || A Blaisdell Book in the History of Science
|-
| Gershenson, Daniel E. ; Greenberg, Daniel A. || ''Anaxagoras and the Birth of Physics'', New York et Londres, Blaisdell, 1964, xxv-538 p. || A History of Physics, série I
|-
| Gigon, Olof || « Zu Anaxagoras », ''Philologus'', vol. 91, 1936-1937, p. 1-41 ; repris dans ''Studien zur antiken Philosophie'', Berlin et New York, De Gruyter, 1972 ||
|-
| Graham, Daniel W. || « The Postulates of Anaxagoras », ''Apeiron'', vol. 27, 1994, p. 77-121 ||
|-
| Graham, Daniel W. || « Empedocles and Anaxagoras: Responses to Parmenides », dans A. A. Long (éd.), ''The Cambridge Companion to Early Greek Philosophy'', Cambridge, Cambridge University Press, 1999, p. 159-180 ||
|-
| Graham, Daniel W. || « Was Anaxagoras a Reductionist? », ''Ancient Philosophy'', vol. 24, 2004, p. 1-18 ||
|-
| Graham, Daniel W. || « Anaxagoras and the Comet », ''Ancient Philosophy'', vol. 33, 2013, p. 1-18 ||
|-
| Graham, Daniel W. ; Hintz, Eric || « Anaxagoras and the Solar Eclipse of 478 BC », ''Apeiron'', vol. 40, 2007, p. 319-344 ||
|-
| Inwood, Brad || « Anaxagoras and Infinite Divisibility », ''Illinois Classical Studies'', vol. 11, 1986, p. 17-33 ||
|-
| Janko, Richard || « Eclipse and Plague: Themistocles, Pericles, Anaxagoras and the Athenians' War on Science », ''Journal of Hellenic Studies'', vol. 140, 2020, p. 213-237 ||
|-
| Kember, Owen || « Anaxagoras' Theory of Sex Differentiation and Heredity », ''Phronesis'', vol. 18, 1973, p. 1-14 ||
|-
| Kerferd, George B. || « Anaxagoras and the Concept of Matter before Aristotle », ''Bulletin of the John Rylands Library'', vol. 52, 1969, p. 129-143 ; repris dans Mourelatos (éd.), 1974, p. 489-503 ||
|-
| Kingsley, Peter || « Notes on Air: Four Questions of Meaning in Empedocles and Anaxagoras », ''Classical Quarterly'', n.s. vol. 45, 1995, p. 26-29 ||
|-
| Laks, André || « Mind's Crisis: On Anaxagoras' ''Nous'' », ''Southern Journal of Philosophy'', vol. 31, suppl., 1993, p. 19-38 ||
|-
| Lesher, James H. || « Mind's Knowledge and Powers of Control in Anaxagoras DK B12 », ''Phronesis'', vol. 40, 1995, p. 125-142 ||
|-
| Lewis, Eric || « Anaxagoras and the Seeds of a Physical Theory », ''Apeiron'', vol. 33, 2000, p. 1-23 ||
|-
| Mann, William E. || « Anaxagoras and the ''Homoiomerē'' », ''Phronesis'', vol. 25, 1980, p. 228-249 ||
|-
| Mansfeld, Jaap || « The Chronology of Anaxagoras' Athenian Period and the Date of His Trial », I, ''Mnemosyne'', vol. 32, 1979, p. 39-69 ; II, ''Mnemosyne'', vol. 33, 1980, p. 17-95 ||
|-
| Mansfeld, Jaap || « Anaxagoras' Other World », ''Phronesis'', vol. 25, 1980, p. 1-4 ||
|-
| Marmodoro, Anna || « Anaxagoras's Qualitative Gunk », ''British Journal for the History of Philosophy'', vol. 23, 2015, p. 402-422 ||
|-
| Marmodoro, Anna || ''Everything in Everything: Anaxagoras's Metaphysics'', New York, Oxford University Press, 2017 ||
|-
| O'Brien, Denis || « The Relation of Anaxagoras and Empedocles », ''Journal of Hellenic Studies'', vol. 88, 1968, p. 93-113 ||
|-
| O'Brien, Denis || « Derived Light and Eclipses in the Fifth Century », ''Journal of Hellenic Studies'', vol. 88, 1968, p. 114-127 ||
|-
| Paxson, Thomas D., Jr. || « The Holism of Anaxagoras », ''Apeiron'', vol. 17, 1983, p. 85-91 ||
|-
| Peck, Arthur L. || « Anaxagoras and the Parts », ''Classical Quarterly'', vol. 20, 1926, p. 57-71 ||
|-
| Peck, Arthur L. || « Anaxagoras: Predication as a Problem in Physics », ''Classical Quarterly'', vol. 25, 1931, p. 27-37 et 112-120 ||
|-
| Pinto, Rhodes || « ''Nous'', Motion, and Teleology in Anaxagoras », ''Oxford Studies in Ancient Philosophy'', vol. 52, 2017, p. 1-32 ||
|-
| Potts, Ronald || « Anaxagoras' Cosmogony », ''Apeiron'', vol. 18, 1984, p. 90-96 ||
|-
| Raven, J. E. || « The Basis of Anaxagoras' Cosmology », ''Classical Quarterly'', n.s. vol. 4, 1954, p. 123-137 ||
|-
| Reesor, Margaret E. || « The Meaning of Anaxagoras », ''Classical Philology'', vol. 55, 1960, p. 1-8 ||
|-
| Rösler, Wolfgang || « ΟΜΟΥ ΧΡΗΜΑΤΑ ΠΑΝΤΑ ΗΝ », ''Hermes'', vol. 99, 1971, p. 246-248 ||
|-
| Schofield, Malcolm || « Doxographica Anaxagorea », ''Hermes'', vol. 103, 1975, p. 1-24 ||
|-
| Schofield, Malcolm || ''An Essay on Anaxagoras'', Cambridge, Cambridge University Press, 1980 || Cambridge Classical Studies
|-
| Sider, David || « Anaxagoras on the Size of the Sun », ''Classical Philology'', vol. 68, 1973, p. 128-129 ||
|-
| Sider, David || « A Note on Anaxagoras, Fr. 1 », 1973, p. 249-251 (revue à préciser) ||
|-
| Sider, David || « Anaxagoras Fr. 14 DK », ''Hermes'', vol. 102, 1974, p. 365-367 ||
|-
| Sisko, John E. || « Anaxagoras' Parmenidean Cosmology: Worlds within Worlds within the One », ''Apeiron'', vol. 36, 2003, p. 87-114 ||
|-
| Sisko, John E. || « Anaxagoras betwixt Parmenides and Plato », ''Philosophy Compass'', vol. 5, 2010, p. 432-442 ||
|-
| Sisko, John E. || « Anaxagoras on Matter, Motion, and Multiple Worlds », ''Philosophy Compass'', vol. 5, 2010, p. 443-454 ||
|-
| Solmsen, Friedrich || « Anaxagoras B 19 Diels-Kranz », ''Hermes'', vol. 91, 1963, p. 251-252 ||
|-
| Stokes, M. C. || « On Anaxagoras. Part I: Anaxagoras' Theory of Matter », ''Archiv für Geschichte der Philosophie'', vol. 47, 1965, p. 1-19 ; « Part II: The Order of Cosmogony », ibid., p. 217-250 ||
|-
| Strang, Colin || « The Physical Theory of Anaxagoras », ''Archiv für Geschichte der Philosophie'', vol. 45, 1963, p. 101-118 ; repris dans Furley et Allen (éd.), vol. II, 1975, p. 361-380 ||
|-
| Tannery, Paul || « La théorie de la matière d'Anaxagore », ''Revue philosophique'', vol. 22, 1886, p. 255-274 ; repris dans ''Pour l'histoire de la science hellène'', Paris, Alcan, 1887 ||
|-
| Taylor, A. E. || « On the Date of the Trial of Anaxagoras », ''Classical Quarterly'', vol. 11, 1917, p. 81-87 ||
|-
| Teodorsson, Sven-Tage || ''Anaxagoras' Theory of Matter'', Göteborg, Acta Universitatis Gothoburgensis, 1982 || Studia Graeca et Latina Gothoburgensia, 42
|-
| Theodossiou, Evangelos Th. ; Niarchos, P. G. ; Manimanis, V. N. ; Orchiston, Wayne || « The Fall of a Meteorite at Aegos Potami in 467/466 BC », ''Journal of Astronomical History and Heritage'', vol. 5, 2002, p. 135-140 ||
|-
| Vlastos, Gregory || « The Physical Theory of Anaxagoras », ''Philosophical Review'', vol. 59, 1950, p. 31-57 ; repris dans Furley et Allen (éd.), vol. II, 1975, p. 323-353, et dans ''Studies in Greek Philosophy'', vol. I, éd. Daniel W. Graham, Princeton, Princeton University Press, 1995, p. 303-327 ||
|-
| Warren, James || « Anaxagoras on Perception, Pleasure, and Pain », ''Oxford Studies in Ancient Philosophy'', vol. 33, 2007, p. 19-54 ||
|-
| Wasserstein, A. || « A Note on Fragment 12 of Anaxagoras », ''Classical Review'', n.s. vol. 10, 1960, p. 4-5 ||
|-
| Weisinger, Kenneth D. || « A Note on Homunculus, Thales, and Anaxagoras », ''Monatshefte'', vol. 64, 1972, p. 237-246 ||
|-
| Woodbury, Leonard || « Anaxagoras and Athens », ''Phoenix'', vol. 35, 1981, p. 295-315 ||
|-
| Woolf, N. J. || « Anaxagoras and the Scientist/Laity Interaction », ''Vistas in Astronomy'', vol. 39, 1995, p. 699-709 ||
|}
=== Comptes rendus ===
{| class="wikitable"
! Auteur !! Référence !! Collection (n°)
|-
| Hall, Marie Boas || « Anaxagoras Revisited », compte rendu de Gershenson et Greenberg, 1964, ''Nature'', vol. 204, 1964 (supplément du 5 décembre), p. 968 ||
|-
| Louguet, Claire || Compte rendu de Curd, ''Anaxagoras of Clazomenae'', 2007, ''Classical Review'', n.s. vol. 59, 2009, p. 23-24 ||
|-
| Naddaf, Gerard || Compte rendu de Graham, ''Science before Socrates'', 2013, ''Mind'', vol. 125, 2016, p. 945-952 ||
|-
| Rawlins, F. I. G. || « Anaxagoras: Philosopher and Artist », compte rendu de Cleve, 1949, ''Nature'', vol. 166, 1950, p. 2-3 ||
|-
| Sider, David || Compte rendu de Teodorsson, ''Anaxagoras' Theory of Matter'', 1982, ''Classical Journal'', vol. 80, 1984, p. 71-73 ||
|}
=== Présocratiques, sciences et contexte ===
{| class="wikitable"
! Auteur !! Référence !! Collection (n°)
|-
| Betegh, Gábor || ''The Derveni Papyrus: Cosmology, Theology and Interpretation'', Cambridge, Cambridge University Press, 2004 ||
|-
| Curd, Patricia || « Presocratic Philosophy », ''Stanford Encyclopedia of Philosophy'', 1{{re}} publication 2007, révision substantielle 2020 ||
|-
| Dover, Kenneth || ''Aristophanes: Clouds'', éd. avec introduction et commentaire, Oxford, Clarendon Press, 1968 ||
|-
| Fränkel, Hermann || ''Wege und Formen frühgriechischen Denkens'', 2{{e}} éd., Munich, Beck, 1960 ||
|-
| Furley, David J. ; Allen, R. E. (éd.) || ''Studies in Presocratic Philosophy'', Londres, Routledge and Kegan Paul, 1970-1975, 2 vol. ||
|-
| Graham, Daniel W. || « La lumière de la lune dans la pensée grecque archaïque », dans André Laks et Claire Louguet (éd.), ''Qu'est-ce que la philosophie présocratique ?'', Lille, Presses universitaires du Septentrion, 2002, p. 351-380 ||
|-
| Graham, Daniel W. || ''Explaining the Cosmos: The Ionian Tradition of Scientific Philosophy'', Princeton, Princeton University Press, 2006 ||
|-
| Graham, Daniel W. || ''Science before Socrates: Parmenides, Anaxagoras, and the New Astronomy'', New York, Oxford University Press, 2013 ||
|-
| Guthrie, W. K. C. || ''A History of Greek Philosophy'', vol. II : ''The Presocratic Tradition from Parmenides to Democritus'', Cambridge, Cambridge University Press, 1965 ||
|-
| Lloyd, G. E. R. || ''Polarity and Analogy: Two Types of Argumentation in Early Greek Thought'', Cambridge, Cambridge University Press, 1966 ||
|-
| Lloyd, G. E. R. || ''Early Greek Science: Thales to Aristotle'', Londres, Chatto & Windus, 1970 ||
|-
| Mourelatos, A. P. D. (éd.) || ''The Pre-Socratics: A Collection of Critical Essays'', Garden City, Anchor Press, 1974 ||
|-
| Norden, Eduard || ''Agnostos Theos. Untersuchungen zur Formengeschichte religiöser Rede'', Leipzig, Teubner, 1913 (4{{e}} tirage, Stuttgart, 1956) ||
|-
| Owen, G. E. L. || « Eleatic Questions », ''Classical Quarterly'', n.s. vol. 10, 1960, p. 84-102 ||
|-
| Owen, G. E. L. || « Plato and Parmenides on the Timeless Present », ''The Monist'', vol. 50, 1966, p. 317-340 ; repris dans Mourelatos (éd.), 1974, p. 271-292 ||
|-
| Sedley, David || « Teleology and Myth in the ''Phaedo'' », ''Proceedings of the Boston Area Colloquium in Ancient Philosophy'', vol. 5, 1989, p. 359-383 ||
|-
| Sedley, David || ''Creationism and Its Critics in Antiquity'', Berkeley, University of California Press, 2007 ||
|-
| Vassallo, Christian || ''The Presocratics at Herculaneum: A Study of Early Greek Philosophy in the Epicurean Tradition'', Berlin et Boston, De Gruyter, 2021 || Studia Praesocratica, 11
|}
=== Influence et réception ===
{| class="wikitable"
! Auteur !! Référence !! Collection (n°)
|-
| Brisson, Luc || ''Le Même et l'Autre dans la structure ontologique du Timée de Platon'', Paris, Klincksieck, 1974 ||
|-
| Chalmers, Alan || « Atomism from the 17th to the 20th Century », ''Stanford Encyclopedia of Philosophy'', 1{{re}} publication 2005 ||
|-
| D'Ancona, Cristina || « Greek into Arabic: Neoplatonism in Translation », dans Peter Adamson et Richard C. Taylor (éd.), ''The Cambridge Companion to Arabic Philosophy'', Cambridge, Cambridge University Press, 2005, p. 10-31 ||
|-
| Gilson, Étienne || ''L'Esprit de la philosophie médiévale'', Paris, Vrin, 1932 ||
|-
| Heisenberg, Werner || ''Physics and Philosophy'', New York, Harper, 1958 (à consulter pour les rapprochements entre présocratiques et physique moderne, à titre d'analogie) ||
|-
| Long, A. A. ; Sedley, D. N. || ''The Hellenistic Philosophers'', vol. I, Cambridge, Cambridge University Press, 1987 ||
|-
| Morrow, Glenn R. || « Necessity and Persuasion in Plato's ''Timaeus'' », ''Philosophical Review'', vol. 59, 1950, p. 147-163 ||
|-
| Pyle, Andrew || ''Atomism and its Critics: From Democritus to Newton'', Bristol, Thoemmes Press, 1995 ||
|-
| Sedley, David || ''Lucretius and the Transformation of Greek Wisdom'', Cambridge, Cambridge University Press, 1998 ||
|-
| White, Michael J. || « Stoic Natural Philosophy (Physics and Cosmology) », dans Brad Inwood (éd.), ''The Cambridge Companion to the Stoics'', Cambridge, Cambridge University Press, 2003, p. 124-152 ||
|}
=== Dictionnaires et encyclopédies ===
{| class="wikitable"
! Auteur ou éditeur !! Référence !! Collection (n°)
|-
| Goulet, Richard (dir.) || ''Dictionnaire des philosophes antiques'', vol. I, Paris, CNRS Éditions, 1989 ||
|}
{{autocat}}
[[Catégorie:Philosophe]]
{{DEFAULTSORT:Anaxagore}}
[[Catégorie:Présocratiques]]
p73hewv5gmcplkz4n5diez4v6eaziwa
773220
773219
2026-09-26T07:57:49Z
PandaMystique
119061
773220
wikitext
text/x-wiki
{{DicoPhilo|Anaxagore de Clazomènes|lecture=oui}}
== Vie et contexte historique ==
{{wikisource|Auteur:Anaxagore_de_Clazomènes|Anaxagore de Clazomènes}}
Les données biographiques concernant Anaxagore sont, selon l'expression de Patricia Curd, « confuses et déroutantes » (''confused and confusing''), et la plupart des anecdotes transmises par la tradition antique doivent être abordées avec une prudence critique<ref>Patricia Curd, ''Anaxagoras of Clazomenae: Fragments and Testimonia'', Toronto, University of Toronto Press, 2007, p. 130. Curd rappelle que les chroniqueurs anciens cherchaient moins l'exactitude que les synchronismes et les filiations de maître à disciple.</ref>. Les récits qui nous sont parvenus proviennent pour l'essentiel de sources tardives : Plutarque (I{{er}}-II{{e}} siècle ap. J.-C.) dans ses ''Vies parallèles'', Valère Maxime (I{{er}} siècle ap. J.-C.) dans ses ''Faits et dits mémorables'', puis Diogène Laërce (III{{e}} siècle ap. J.-C.) dans ses ''Vies et doctrines des philosophes illustres''. Ces auteurs compilent des traditions antérieures (Démétrios de Phalère, Apollodore, Sotion, Satyros, Hermippe), parfois stylisées à des fins rhétoriques ou morales, parfois légendaires.
Anaxagore (en grec ancien Ἀναξαγόρας) naît vers 500 av. J.-C. à Clazomènes, cité grecque d'Ionie, sur la côte occidentale de l'Asie Mineure<ref>Diogène Laërce, II, 7 (DK 59 A 1) : selon Apollodore, il naquit lors de la 70{{e}} Olympiade (500-497 av. J.-C.) et mourut la première année de la 88{{e}} (428/7). La date de naissance vers 500 fait l'objet d'un large accord : Curd, 2007, p. 130 ; Malcolm Schofield, ''An Essay on Anaxagoras'', Cambridge, Cambridge University Press, 1980, p. 33 ; David Sider, ''The Fragments of Anaxagoras'', Meisenheim am Glan, Hain, 1981, p. 1-2.</ref>. Fils d'Hégésibule (certaines sources disent Eubule), il appartient, selon la tradition, à une famille aristocratique et fortunée<ref>Diogène Laërce, II, 6 (DK 59 A 1).</ref>. Diogène Laërce rapporte qu'il céda son patrimoine à ses proches et qu'il se consacra à l'étude de la nature en se tenant à l'écart des affaires publiques ; à ceux qui lui reprochaient de négliger ses biens, il aurait répondu : « Pourquoi ne vous en occupez-vous pas vous-mêmes ? »<ref>Diogène Laërce, II, 6-7 (DK 59 A 1). Platon, ''Hippias majeur'', 283a, et Plutarque, ''Vie de Périclès'', 16, évoquent aussi la négligence d'Anaxagore envers son héritage (DK 59 A 13).</ref>. Valère Maxime rapporte de son côté qu'Anaxagore, revenant d'un long voyage et trouvant ses terres à l'abandon, aurait déclaré : « Je ne serais pas sauf si elles n'avaient pas péri »<ref>Valère Maxime, ''Faits et dits mémorables'', VIII, 7, ext. 6 (DK 59 A 31). Diogène Laërce ne rapporte pas ce mot. Valère Maxime y voit une parole empreinte de sagesse.</ref>.
Ces récits, qu'ils soient authentiques ou légendaires, illustrent l'image qu'Anaxagore a laissée : celle du philosophe contemplatif, détaché des affaires domestiques et tourné vers l'étude du cosmos. À quelqu'un qui lui demandait s'il n'avait aucun souci de sa patrie, il aurait répondu, en montrant le ciel : « Tais-toi, j'ai le plus grand souci de ma patrie »<ref>Diogène Laërce, II, 7 (DK 59 A 1).</ref>. Interrogé sur l'homme le plus heureux, il aurait dit qu'aucun de ceux auxquels on songe ne mérite ce titre, et que l'homme heureux paraîtrait étrange à son interlocuteur<ref>Aristote, ''Éthique à Eudème'', I, 4, 1215b6-8 ; cf. ''Éthique à Nicomaque'', X, 9, 1179a13-16 (DK 59 A 30).</ref>. À qui lui demandait pourquoi l'on devrait choisir de naître plutôt que de ne pas être, il aurait répondu : « Pour contempler le ciel et l'ordre de l'univers entier »<ref>Aristote, ''Éthique à Eudème'', I, 5, 1216a11-14 (DK 59 A 30). Schofield (1980, p. 22-23) rapproche ces anecdotes du portrait platonicien du philosophe dans la digression du ''Théétète'' et en souligne le caractère stylisé.</ref>.
=== L'arrivée à Athènes et l'activité philosophique ===
La chronologie de la vie d'Anaxagore reste discutée, en raison des divergences entre les sources anciennes<ref>Leonard Woodbury, « Anaxagoras and Athens », ''Phoenix'', vol. 35, 1981, p. 295-315 ; Jaap Mansfeld, « The Chronology of Anaxagoras' Athenian Period and the Date of His Trial », ''Mnemosyne'', vol. 32, 1979, p. 39-69, et vol. 33, 1980, p. 17-95 ; Schofield, 1980, p. 33-35 ; Sider, 1981, p. 1-11.</ref>. Diogène Laërce rapporte, d'après Démétrios de Phalère, qu'Anaxagore commença à philosopher à Athènes à l'âge de vingt ans, sous l'archontat de Callias, et qu'il y séjourna trente ans ; il le dit aussi âgé de vingt ans lors du passage de Xerxès (480 av. J.-C.)<ref>Diogène Laërce, II, 7 (DK 59 A 1). Le nom de l'archonte pose problème : Callias correspond à 456/5, tandis que la correction « Calliadès » (480/79) s'accorde avec l'âge de vingt ans. Voir Sider, 1981, p. 2-4 ; Curd, 2007, p. 78, n. 1, et p. 131.</ref>. Une arrivée précoce, vers 480, est défendue par Leonard Woodbury et Daniel Graham ; Jaap Mansfeld retient 456/5 et un séjour de vingt ans, date qu'Olof Gigon admettait déjà et que suit Curd ; d'autres proposent des dates intermédiaires<ref>Woodbury, 1981, p. 299 et 306 ; Olof Gigon, « Zu Anaxagoras », ''Philologus'', vol. 91, 1936-1937, p. 1-41, repris dans ''Studien zur antiken Philosophie'', Berlin et New York, De Gruyter, 1972 ; Daniel W. Graham, ''Explaining the Cosmos: The Ionian Tradition of Scientific Philosophy'', Princeton, Princeton University Press, 2006 ; Mansfeld, 1979, p. 39 ; Curd, 2007, p. 131. Pour une présentation récente des hypothèses (480, 478, 464, 460, 456), voir Patricia Curd et John Sisko, « Anaxagoras », ''Stanford Encyclopedia of Philosophy'', 2007, révision substantielle 2026, § 1.</ref>. Les sources attribuent au séjour athénien une durée de vingt ou de trente ans ; selon la chronologie retenue, on le situe approximativement entre 480 et 450 ou entre 456 et 437 av. J.-C. Schofield, de son côté, place la composition du livre vers 470-460 av. J.-C.<ref>Curd, 2007, p. 129 ; Schofield, 1980, p. 33-35.</ref>. Aristote dit d'Anaxagore qu'il était antérieur à Empédocle par l'âge, mais postérieur par ses œuvres, formule dont le sens est discuté ; Denis O'Brien a soutenu, en s'appuyant sur Alcidamas, selon qui Empédocle avait été l'auditeur d'Anaxagore, que celui-ci écrivit le premier et influença Empédocle<ref>Aristote, ''Métaphysique'', A, 3, 984a11-13 (DK 59 A 43) ; Diogène Laërce, VIII, 56 ; Denis O'Brien, « The Relation of Anaxagoras and Empedocles », ''Journal of Hellenic Studies'', vol. 88, 1968, p. 93-113, ici p. 93-94.</ref>.
Quelle que soit la date de son arrivée, Anaxagore est présenté par la tradition comme le premier philosophe à s'être établi à Athènes et à y avoir apporté la recherche ionienne sur la nature<ref>Clément d'Alexandrie, ''Stromates'', I, 63 (DK 59 A 7) : Anaxagore aurait transporté d'Ionie à Athènes l'enseignement d'Anaximène. La filiation avec Anaximène est chronologiquement peu vraisemblable (Curd, 2007, p. 129, n. 1).</ref>. Athènes, après ses victoires sur les Perses à Marathon (490) et à Salamine (480), devient alors l'un des principaux centres politiques et intellectuels du monde grec. C'est dans ce contexte que la tradition situe l'enseignement d'Anaxagore.
La tradition biographique, dominée par le récit de Plutarque, lie Anaxagore à Périclès<ref>Plutarque, ''Vie de Périclès'', 4-6, 16, 32 (DK 59 A 15-17) ; Platon, ''Phèdre'', 269e-270a (DK 59 A 15) ; Isocrate, ''Sur l'échange'', 235 (DK 59 A 15), qui fait de Périclès le disciple d'Anaxagore et de Damon ; Diodore de Sicile, XII, 39 (DK 59 A 17), qui appelle Anaxagore « le maître de Périclès ».</ref>. Curd juge cette amitié « bien établie », mais Schofield invite à ne pas prendre trop au sérieux le titre d'« élève » donné à Périclès : il en retient seulement la vraisemblance d'une influence exercée dans la jeunesse de l'homme d'État<ref>Curd, 2007, p. 132 ; Schofield, 1980, p. 34. Sider (1981, p. 3) rappelle que ce genre de récit reflète souvent la seule conviction qu'un homme plus jeune a appris de son aîné.</ref>. L'image d'un philosophe naturaliste qui aurait donné au grand orateur démocratique son éloquence élevée provient d'abord du ''Phèdre'' de Platon ; elle appartient en partie à la construction biographique et ne doit pas être surinterprétée.
Parmi les auditeurs d'Anaxagore, la tradition mentionne le poète Euripide<ref>Diogène Laërce, II, 10 (DK 59 A 1) ; Alexandre d'Étolie, cité par Aulu-Gelle, ''Nuits attiques'', XV, 20 (DK 59 A 21). Curd (2007, p. 132-133, n. 13) souligne que la présence d'échos anaxagoréens chez Euripide n'implique pas une relation formelle de maître à élève.</ref> et Archélaos, qui aurait ensuite été le maître de Socrate<ref>Diogène Laërce, II, 16 (DK 60 A 1) ; Curd, 2007, p. 134 et n. 18.</ref>. La question de savoir si Socrate a rencontré Anaxagore reste ouverte. Dans le ''Phédon'', Socrate dit avoir entendu quelqu'un lire le livre d'Anaxagore, puis l'avoir lu lui-même<ref>Platon, ''Phédon'', 97b-99d (DK 59 A 47). Platon ne nomme pas le lecteur ; l'hypothèse selon laquelle il s'agirait d'Archélaos est une conjecture moderne (Curd, 2007, p. 134 ; Burnet la formule avec un point d'interrogation).</ref>. Certains en ont conclu qu'Anaxagore avait quitté Athènes avant que Socrate ne s'intéresse à la philosophie ; Curd juge cet argument du silence peu probant<ref>Curd, 2007, p. 134-136 ; en sens contraire, Woodbury, 1981, p. 297, et Schofield, 1980, p. 34-35.</ref>. Dans l’''Apologie'', Socrate rappelle que les livres d'Anaxagore pouvaient s'acquérir pour une drachme au plus « à l'orchestra »<ref>Platon, ''Apologie de Socrate'', 26d-e (DK 59 A 35). Sur l'interprétation de ce passage (achat de livres ou de lectures), voir Curd, 2007, p. 92, n. 12.</ref>, ce qui atteste la diffusion de ses idées à Athènes à la fin du V{{e}} siècle.
=== L'œuvre écrite ===
Les témoignages antiques et l'étude de la transmission favorisent l'hypothèse qu'Anaxagore n'a publié qu'un seul ouvrage en prose ionienne, désigné diversement dans les sources : Περὶ φύσεως, τὰ Φυσικά, ou simplement son « ouvrage » ou ses « livres » ; Simplicius cite ce qu'il appelle le « premier livre »<ref>Diogène Laërce, I, 16 (DK 59 A 37), range Anaxagore parmi les auteurs d'un seul livre, dans une liste peu sûre ; Simplicius, ''Commentaire sur la Physique d'Aristote'', 34, 29 et 155, 26 ; Sider, 1981, p. 11-13, qui conclut en faveur d'un ouvrage unique.</ref>. Platon emploie le pluriel τὰ Ἀναξαγόρου βιβλία dans l’''Apologie''<ref>Platon, ''Apologie'', 26d.</ref> : Burnet y voyait l'indice d'un ouvrage occupant plusieurs rouleaux, ce que Raven contestait ; pour Sider, ces « livres » désignent plutôt les parties d'un même ouvrage<ref>Sider, 1981, p. 12-13 ; John Burnet, ''Early Greek Philosophy'', 4{{e}} éd., Londres, A. & C. Black, 1930, chap. VI ; Sven-Tage Teodorsson, ''Anaxagoras' Theory of Matter'', Göteborg, Acta Universitatis Gothoburgensis, 1982, p. 10.</ref>. Les autres écrits qu'on lui attribue (sur la quadrature du cercle, sur la perspective, un recueil de problèmes) ne peuvent être établis comme des ouvrages indépendants, et plusieurs de ces attributions sont douteuses ; le traité de perspective pourrait correspondre à un sujet abordé par Anaxagore plutôt qu'à un livre distinct<ref>DK 59 A 38-40 ; Sider, 1981, p. 11-13 ; Teodorsson, 1982, p. 10 ; Burnet, 1930, chap. VI.</ref>. Diogène Laërce rapporte encore qu'Anaxagore fut le premier à publier un livre accompagné de figures<ref>Diogène Laërce, II, 11 (DK 59 A 1).</ref>.
Diogène Laërce qualifie son style d'agréable et d'élevé<ref>Diogène Laërce, II, 6 (DK 59 A 1).</ref>. Schofield a analysé la texture de cette prose archaïque : syntaxe paratactique, répétitions, et, dans le fragment B12, un style de « prédication solennelle » qui accumule les attributs du Noûs à la manière d'un hymne<ref>Schofield, 1980, p. 6-9, qui s'appuie sur Karl Deichgräber, « Hymnische Elemente in der philosophischen Prosa der Vorsokratiker », ''Philologus'', vol. 88, 1933, p. 347-361, et renvoie à Eduard Norden et Hermann Fränkel.</ref>. Contrairement à Parménide et à Empédocle, qui écrivent en hexamètres, Anaxagore s'inscrit dans la tradition de la prose ionienne<ref>Jochen Althoff, « Presocratic Discourse in Poetry and Prose: The Case of Empedocles and Anaxagoras », ''Studies in History and Philosophy of Science'', vol. 43, 2012, p. 293-299.</ref>. Il subsiste de ce traité une vingtaine de fragments, conservés pour la plupart par Simplicius de Cilicie (VI{{e}} siècle ap. J.-C.) dans ses commentaires sur Aristote<ref>Édition de référence : Hermann Diels et Walther Kranz, ''Die Fragmente der Vorsokratiker'', 6{{e}} éd., Berlin, Weidmann, 1951-1952, vol. II, p. 5-44 (chapitre 59, témoignages A et fragments B). Édition plus récente, avec une nouvelle numérotation (Anaxagore y porte le numéro 25) : André Laks et Glenn W. Most, ''Early Greek Philosophy'', 9 vol., Cambridge (Mass.), Harvard University Press, 2016 ; version française : ''Les Débuts de la philosophie'', Paris, Fayard, 2016. Voir aussi Curd, 2007, et Sider, 1981 (2{{e}} éd. 2005). Le fragment B20 est tenu pour inauthentique depuis Sider (Claire Louguet, compte rendu de Curd, 2007, ''Classical Review'', n.s. vol. 59, 2009, p. 23-24).</ref>. Les citations de Simplicius semblent limitées au premier livre, consacré aux principes généraux<ref>Burnet, 1930, chap. VI ; Teodorsson, 1982, p. 10. Schofield (1980, p. 159, n. 36) doute que Simplicius ait disposé du livre entier plutôt que d'extraits (cf. Curd, 2007, p. 165, n. 25).</ref>.
=== Le procès et l'exil ===
Les circonstances du départ d'Anaxagore d'Athènes sont particulièrement controversées. Diogène Laërce rapporte plusieurs versions de son procès<ref>Diogène Laërce, II, 12-14 (DK 59 A 1).</ref>. Selon Sotion, Anaxagore fut accusé d'impiété (ἀσέβεια) par Cléon pour avoir soutenu que le soleil était une masse de métal incandescent ; défendu par Périclès, il fut condamné à une amende de cinq talents et à l'exil<ref>Sotion, dans Diogène Laërce, II, 12.</ref>. Selon Satyros, l'accusateur fut Thucydide, fils de Mélésias, adversaire politique de Périclès, et l'accusation porta sur l'impiété et le médisme ; Anaxagore aurait été condamné à mort par contumace<ref>Satyros, dans Diogène Laërce, II, 12.</ref>. Hermippe le dit emprisonné en attente d'exécution puis libéré à la demande de Périclès ; Hiéronymos rapporte que Périclès le présenta au tribunal affaibli par la maladie, si bien qu'il fut acquitté par pitié<ref>Hermippe et Hiéronymos, dans Diogène Laërce, II, 13-14.</ref>.
Plutarque rapporte qu'un certain Diopeithès fit voter un décret autorisant les poursuites contre ceux qui ne reconnaissaient pas les dieux ou enseignaient des doctrines sur les phénomènes célestes, afin de jeter le soupçon sur Périclès à travers Anaxagore ; Périclès, craignant pour Anaxagore, l'éloigna de la cité<ref>Plutarque, ''Vie de Périclès'', 32, 1-5 (DK 59 A 17). Plutarque ne précise pas le contenu des doctrines incriminées. La thèse du soleil-pierre et de la lune-terre est mentionnée par Platon, ''Apologie'', 26d (DK 59 A 35), où Mélétos attribue ces opinions à Socrate. Plutarque (''Vie de Nicias'', 23 = DK 59 A 18) dit aussi que Périclès eut peine à tirer Anaxagore de prison.</ref>. Johannes Geffcken jugeait le décret de Diopeithès historiquement fondé et estimait que l'accusation visait l'ensemble de la « météorologie » d'Anaxagore, notamment son explication naturelle de la foudre, plus que la seule thèse du soleil<ref>Johannes Geffcken, « Die Asebeia des Anaxagoras », ''Hermes'', vol. 42, 1907, p. 127-133, ici p. 133.</ref>.
L'historicité du procès a été mise en doute, notamment par K. J. Dover, pour qui aucune des sources anciennes ne savait réellement ce qui était arrivé à Anaxagore ; J. A. Davison a au contraire reconstitué une vie comportant deux procès, l'un vers 456/5, l'autre vers 433-430, ce qui l'oblige à supposer une amnistie dont rien ne témoigne<ref>K. J. Dover, « The Freedom of the Intellectual in Greek Society », ''Talanta'', vol. 7, 1975, p. 24-54 (surtout p. 27-32), cité par Schofield, 1980, p. 34 et n. 74 ; J. A. Davison, « Protagoras, Democritus, and Anaxagoras », ''Classical Quarterly'', n.s. vol. 3, 1953, p. 33-45 (surtout p. 39-45), suivi par Guthrie et Russell Meiggs ; critique de cette reconstruction chez Woodbury, 1981, p. 303 (en note). Gershenson et Greenberg, dans ''Anaxagoras and the Birth of Physics'' (1964), nient également le procès (Teodorsson, 1982, p. 7, n. 2).</ref>. Curd estime néanmoins qu'il y a peu de raisons de douter de la tradition sur ce point<ref>Curd, 2007, p. 136.</ref>. La date reste incertaine : vers 450 selon A. E. Taylor et Burnet, au milieu du siècle selon Woodbury, qui rattache l'accusation portée par Thucydide, fils de Mélésias, aux luttes politiques de cette période, en 437/6 selon Mansfeld, vers 434 selon Sider, vers 430 selon Richard Janko<ref>A. E. Taylor, « On the Date of the Trial of Anaxagoras », ''Classical Quarterly'', vol. 11, 1917, p. 81-87 ; Burnet, 1930, chap. VI ; Woodbury, 1981, p. 315 ; Mansfeld, 1979, p. 39 ; Sider, 1981, p. 1-11 ; Richard Janko, « Eclipse and Plague: Themistocles, Pericles, Anaxagoras and the Athenians' War on Science », ''Journal of Hellenic Studies'', vol. 140, 2020, p. 213-237. Pour la synthèse, voir Curd et Sisko, ''SEP'', 2026, § 1.</ref>.
Anaxagore se retira ensuite à Lampsaque, sur l'Hellespont (l'actuel détroit des Dardanelles), où il fut honoré<ref>Diogène Laërce, II, 14-15 (DK 59 A 1) ; Alcidamas, cité par Aristote, ''Rhétorique'', II, 23, 1398b15-16 (DK 59 A 23).</ref>. Il y mourut en 428/7 av. J.-C., à soixante-douze ans selon une tradition que rapporte Diogène Laërce<ref>Diogène Laërce, II, 7 (DK 59 A 1) : l'âge de soixante-douze ans vient d'une tradition anonyme (« on dit »), la date de la mort d'Apollodore ; voir la traduction du passage chez Mansfeld, 1979, p. 41.</ref>. Invité par les magistrats de la cité à exprimer un vœu, il aurait demandé que les enfants aient congé chaque année dans le mois de sa mort, usage encore observé au temps de Diogène Laërce<ref>Diogène Laërce, II, 14 (DK 59 A 1).</ref>. Élien rapporte qu'un autel lui fut élevé, portant les inscriptions « Intellect » (Νοῦς) et « Vérité » (Ἀλήθεια)<ref>Élien, ''Histoire variée'', VIII, 19 (DK 59 A 24). Le texte peut signifier soit un autel portant une inscription sur chaque face, soit un autel dédié aux deux (Curd, 2007, p. 88, n. 10).</ref>. L'épitaphe gravée sur sa tombe nous est parvenue : « Ci-gît Anaxagore, qui atteignit le plus lointain terme de la vérité sur le cosmos céleste »<ref>Diogène Laërce, II, 15 (DK 59 A 1) ; Élien, ''Histoire variée'', VIII, 19 (DK 59 A 24). Alcidamas, au IV{{e}} siècle, atteste que les Lampsacéniens l'honoraient encore (DK 59 A 23) ; Gershenson et Greenberg (1964, p. 4) rappellent que sa mémoire fut honorée pendant plus d'un siècle.</ref>. Cicéron rapporte enfin qu'à des amis qui lui demandaient, à Lampsaque, s'il voulait être ramené à Clazomènes, il aurait répondu que le chemin des Enfers est le même de partout<ref>Cicéron, ''Tusculanes'', I, 43, 104 (DK 59 A 34a) ; cf. Diogène Laërce, II, 11.</ref>.
=== Portrait et anecdotes ===
Les sources anciennes conservent de nombreuses anecdotes qui, sans garantie historique, témoignent de l'image du philosophe dans la tradition. Apprenant la mort de son fils, Anaxagore aurait dit avec le plus grand calme : « Je savais que j'avais engendré un mortel »<ref>Galien, ''Des opinions d'Hippocrate et de Platon'', IV, 7 (DK 59 A 33) ; Diogène Laërce, II, 13, rapporte le mot au pluriel, à propos de ses enfants.</ref>. Ces récits ont fait de lui, dans la tradition, le modèle du sage détaché des contingences et consacré à la contemplation de la nature.
Dans le ''Phèdre'', Socrate attribue l'élévation de l'éloquence de Périclès à sa fréquentation d'Anaxagore, dont les discours portaient pour l'essentiel sur la nature de l'intelligence et de la déraison (νοῦ τε καὶ ἀνοίας)<ref>Platon, ''Phèdre'', 269e-270a (DK 59 A 15) ; Schofield, 1980, p. 22-23.</ref>. Cette remarque témoigne de la réputation d'Anaxagore dans l'Athènes classique : celle d'un penseur dont les spéculations « météorologiques » (au sens ancien de l'étude des phénomènes célestes) pouvaient paraître étranges au commun. Les ''Nuées'' d'Aristophane tournent en ridicule la nouvelle science de la nature ; toutefois, selon Dover, que suit Schofield, les idées moquées dans la pièce se rattachent plus aisément à Diogène d'Apollonie qu'à Anaxagore<ref>Aristophane, ''Les Nuées'' ; Kenneth Dover, ''Aristophanes: Clouds'', Oxford, Clarendon Press, 1968, introduction ; Schofield, 1980, p. 35 et n. 83 ; Curd, 2007, p. 132, n. 11. Gábor Betegh a proposé de voir aussi dans la pièce des échos d'Archélaos (Betegh, 2016).</ref>.
=== Les dernières années à Lampsaque ===
Anaxagore poursuivit vraisemblablement son activité à Lampsaque, où il jouit d'un grand prestige. Certains historiens, notamment Schofield, y reconstruisent une école anaxagoréenne ; les témoignages ne permettent toutefois pas d'établir qu'il y eut une école organisée au sens propre<ref>Schofield, 1980, p. 35 et n. 84, qui s'appuie sur Diogène Laërce, II, 14-15, Aristote, ''Rhétorique'', 1398b15 (DK 59 A 23), et Eusèbe, ''Préparation évangélique'', X, 14, 13 (DK 59 A 7) ; Sider, 1981, p. 3-4 ; Burnet, 1930, chap. VI. Curd (2007, p. 129, n. 1) rappelle que nous ignorons la nature exacte de son activité philosophique, et notamment s'il eut une école.</ref>. Eusèbe rapporte qu'Archélaos lui aurait succédé à la tête de cette école<ref>Eusèbe, ''Préparation évangélique'', X, 14, 13 (DK 59 A 7). Cette indication s'accorde mal avec les témoignages qui situent Archélaos à Athènes ; Curd (2007, p. 134, n. 18) note que la tradition a pu confondre certains aspects de la vie des deux hommes.</ref>. Parmi ses proches, les sources mentionnent aussi Métrodore de Lampsaque, qui prolongea l'interprétation d'Homère attribuée à Anaxagore en étudiant la « physique » du poète<ref>Diogène Laërce, II, 11 (DK 59 A 1), d'après Favorinus ; DK 61. Schofield (1980, p. 149, n. 59) range Archélaos (DK 60 A 1-5) et Métrodore (DK 61 A 2, 6) parmi ses disciples.</ref>. Cette présence à Lampsaque a pu contribuer à la circulation de ses idées après son départ d'Athènes.
== Les principes métaphysiques ==
La philosophie d'Anaxagore se construit pour une large part en réponse aux exigences posées par Parménide d'Élée<ref>Curd, 2007, p. 137-142 ; Patricia Curd, ''The Legacy of Parmenides: Eleatic Monism and Later Presocratic Thought'', Princeton, Princeton University Press, 1998 (rééd. Las Vegas, Parmenides Publishing, 2004) ; Schofield, 1980, p. 5.</ref>. Dans son poème, Parménide oppose deux voies de recherche : celle « qu'il est et qu'il n'est pas possible qu'il ne soit pas » (ὅπως ἔστιν τε καὶ ὡς οὐκ ἔστι μὴ εἶναι) et celle « qu'il n'est pas et qu'il faut qu'il ne soit pas »<ref>Parménide, DK 28 B 2, 3-5 ; cf. B 6, 1-2 (ἔστι γὰρ εἶναι, μηδὲν δ' οὐκ ἔστιν, « car être est, et le néant n'est pas »).</ref>. Il en conclut que toute génération et toute corruption véritables sont impossibles, car elles supposeraient un passage de ce qui n'est pas à ce qui est, ou l'inverse<ref>Parménide, B 8, 6-21 ; cf. Aristote, ''Physique'', I, 8, 191a23-31.</ref>. Dans l'interprétation traditionnelle, Parménide conclut que ce qui est véritablement est un, continu, homogène et immobile<ref>Parménide, B 8, 22 : « Il n'est pas divisible, puisqu'il est tout entier semblable » (οὐδὲ διαιρετόν ἐστιν, ἐπεὶ πᾶν ἐστιν ὁμοῖον).</ref>. Cette lecture comme monisme numérique a toutefois été contestée, notamment par Patricia Curd, qui comprend plutôt les caractères énoncés en B8 comme les conditions auxquelles doit satisfaire toute réalité fondamentale ; John Sisko estime au contraire qu'Anaxagore construit sa physique contre un monisme numérique qu'il attribue à Parménide<ref>Curd, 1998 ; Curd, 2007, p. 140-141, qui rappelle qu'Aristote distinguait déjà l'unité selon la définition chez Parménide et l'unité selon la matière chez Mélissos (''Métaphysique'', A, 5, 986b10-11 et 19-20) ; John E. Sisko, « Anaxagoras betwixt Parmenides and Plato », ''Philosophy Compass'', vol. 5, 2010, p. 432-442, ici p. 432.</ref>.
Anaxagore accepte l'impossibilité de la génération à partir de ce qui n'est pas, mais refuse d'en conclure que la pluralité et le changement sont illusoires<ref>Aristote, ''Métaphysique'', A, 3, 984a11-16 (DK 59 A 43) ; ''Physique'', I, 4, 187a26-29 (DK 59 A 52).</ref>. Théophraste, cité par Simplicius, dit qu'Anaxagore, qui avait partagé la philosophie d'Anaximène, fut le premier à modifier les doctrines sur les principes et à fournir la cause qui leur manquait<ref>Théophraste, dans Simplicius, ''Commentaire sur la Physique'', 27, 2-4 (DK 59 A 41). La « cause qui manquait » désigne vraisemblablement le Noûs, cause du mouvement (Curd, 2007, p. 93, n. 13). Le lien avec Anaximène doit s'entendre au sens large d'une appartenance à la tradition ionienne (Curd, 2007, p. 129, n. 1 ; Burnet, 1930, chap. VI).</ref>. Plutôt que de nier le devenir, Anaxagore cherche à fonder une cosmologie qui satisfasse les exigences éléates tout en rendant compte de la multiplicité et du mouvement observables. Schofield souligne toutefois qu'il reste un cosmologue ionien, qui intègre certaines thèses de Parménide sans adopter sa méthode argumentative<ref>Schofield, 1980, p. 5 et 26-28 ; G. E. L. Owen, « Eleatic Questions », ''Classical Quarterly'', n.s. vol. 10, 1960, p. 84-102. Sur le débat entre ceux qui voient en Anaxagore un critique de Parménide et ceux qui en font un disciple, voir d'un côté Daniel W. Graham, « Empedocles and Anaxagoras: Responses to Parmenides », dans A. A. Long (éd.), ''The Cambridge Companion to Early Greek Philosophy'', Cambridge, Cambridge University Press, 1999, p. 159-180, et de l'autre John E. Sisko, « Anaxagoras' Parmenidean Cosmology: Worlds within Worlds within the One », ''Apeiron'', vol. 36, 2003, p. 87-114 ; voir aussi Curd et Sisko, ''SEP'', 2026, § 2.</ref>. G. E. L. Owen a relevé que la première phrase du traité, « Toutes choses étaient ensemble », est « manifestement formulée comme une contradiction plate de Parménide » sur plusieurs points essentiels<ref>G. E. L. Owen, « Plato and Parmenides on the Timeless Present », ''The Monist'', vol. 50, 1966, p. 317-340, repris dans A. P. D. Mourelatos (éd.), ''The Pre-Socratics'', Garden City, Anchor Press, 1974, p. 271-292, ici p. 276-277 ; cité par Schofield, 1980, p. 64 et n. 51.</ref>.
=== Génération, mélange et dissociation ===
Le premier principe d'Anaxagore est énoncé dans le fragment B17 : « Les Grecs ne pensent pas correctement la naissance et la destruction : aucune chose ne naît ni ne périt, mais, à partir des choses qui sont, il y a mélange et dissociation. Ainsi auraient-ils raison d'appeler la naissance “mélange” et la destruction “dissociation” » (τὸ γίνεσθαι συμμίσγεσθαι καὶ τὸ ἀπόλλυσθαι διακρίνεσθαι)<ref>Anaxagore, fragment B17, cité par Simplicius, ''Commentaire sur la Physique'', 163, 18-24 (DK 59 B 17 ; LM 25 D15). Jochen Althoff compare ce fragment en prose au fragment 31 B8 d'Empédocle, qui exprime en vers une idée voisine (« Presocratic Discourse in Poetry and Prose: The Case of Empedocles and Anaxagoras », ''Studies in History and Philosophy of Science'', vol. 43, 2012, p. 293-299).</ref>. Ce principe constitue la base de sa physique et sa réponse à l'interdit parménidien. Ce qui apparaît comme génération n'est qu'un réarrangement d'ingrédients préexistants qui se mélangent (συμμίσγεται) ; ce qui paraît destruction n'est que leur dissociation (διακρίνεται)<ref>Aristote, ''Métaphysique'', A, 3, 984a13-16 (DK 59 A 43) : selon Anaxagore, presque tous les homéomères naissent et périssent seulement par agrégation et dissociation. Curd (2007, p. 145) rappelle que l'assimilation de la génération à l'altération, qu'Aristote prête à Anaxagore (''Génération et corruption'', I, 1, 314a11-13), est une interprétation aristotélicienne.</ref>. Rien ne naît du néant, rien n'y retourne : les ingrédients de la réalité existent de toute éternité et conservent leur nature propre.
Aristote explique la position d'Anaxagore par son adhésion à l'opinion commune des physiciens selon laquelle rien ne naît de ce qui n'est pas<ref>Aristote, ''Physique'', I, 4, 187a26-29 (DK 59 A 52) ; Schofield, 1980, p. 43-44.</ref>. Le principe prend une forme plus précise dans une question que la tradition attribue à Anaxagore : « Comment le cheveu pourrait-il naître de ce qui n'est pas cheveu, et la chair de ce qui n'est pas chair ? » (πῶς γὰρ ἂν ἐκ μὴ τριχὸς γένοιτο θρὶξ καὶ σὰρξ ἐκ μὴ σαρκός;)<ref>Anaxagore, fragment B10, transmis par une scholie à Grégoire de Nazianze (DK 59 B 10). L'authenticité de la formule est discutée : Schofield l'a d'abord contestée (« Doxographica Anaxagorea », ''Hermes'', vol. 103, 1975, p. 1-24), puis a admis que le scholiaste, dont la source pourrait être Eudème, en conserve peut-être les termes (Schofield, 1980, p. 133-143). La plupart des spécialistes la tiennent pour authentique (Sider ; Curd, 2007, p. 53-54).</ref>. Cette formule exprime ce que les commentateurs appellent le principe « du semblable par le semblable » : une substance ne peut provenir que de la même substance, déjà présente sous une forme non manifeste<ref>Schofield, 1980, p. 44 et 55-58, qui analyse ce principe dans la reconstruction aristotélicienne.</ref>.
On rapproche parfois ce principe de la loi de conservation de la matière formulée par Antoine-Laurent de Lavoisier, que résume souvent une formule moderne, non littérale chez Lavoisier : « Rien ne se perd, rien ne se crée, tout se transforme ». L'analogie ne doit pas être surinterprétée : les mécanismes qu'Anaxagore propose pour expliquer les transformations (mélange et dissociation d'ingrédients éternels) sont qualitatifs et ne relèvent pas d'une chimie quantitative au sens moderne<ref>Antoine-Laurent de Lavoisier, ''Traité élémentaire de chimie'', Paris, Cuchet, 1789, t. I, chap. XIII, où est posé le principe d'une égale quantité de matière avant et après toute opération. Le rapprochement avec Anaxagore relève de l'analogie rétrospective, non d'une filiation historique.</ref>. L'intuition d'Anaxagore partage avec le principe de Lavoisier l'idée que rien ne se perd ni ne se crée dans le devenir physique ; il serait anachronique d'y voir davantage.
=== Tout est dans tout ===
Le deuxième principe s'énonce ainsi : « En toute chose il y a une part de toute chose, sauf de l'Intellect ; et il est des choses dans lesquelles l'Intellect aussi est présent » (ἐν παντὶ παντὸς μοῖρα ἔνεστι πλὴν νοῦ, ἔστιν οἷσι δὲ καὶ νοῦς ἔνι)<ref>Anaxagore, fragment B11, cité par Simplicius, ''Commentaire sur la Physique'', 164, 23-24 (DK 59 B 11). La même thèse figure en B6 et au début de B12.</ref>. Ce principe, souvent désigné par la formule latine ''omnia in omnibus'', est la thèse la plus caractéristique et la plus déroutante de la philosophie d'Anaxagore ; Curd le nomme « principe du mélange universel »<ref>Curd, 2007, p. 179 et n. 3 ; cf. Gregory Vlastos, « The Physical Theory of Anaxagoras », ''Philosophical Review'', vol. 59, 1950, p. 31-57 ; Colin Strang, « The Physical Theory of Anaxagoras », ''Archiv für Geschichte der Philosophie'', vol. 45, 1963, p. 101-118.</ref>. Il signifie que toute portion du mélange, si petite soit-elle, contient des parts de tous les ingrédients qui existent : aucun ingrédient n'existe à l'état pur, isolé de tous les autres<ref>Anaxagore, B6 : « Puisqu'il n'est pas possible qu'il y ait un plus petit, rien ne pourrait être séparé ni venir à être par soi-même, mais, comme au commencement, maintenant aussi toutes choses sont ensemble » (ὅτε τοὐλάχιστον μὴ ἔστιν εἶναι, οὐκ ἂν δύναιτο χωρισθῆναι, οὐδ' ἂν ἐφ' ἑαυτοῦ γενέσθαι, ἀλλ' ὅπωσπερ ἀρχὴν εἶναι καὶ νῦν πάντα ὁμοῦ).</ref>. Ce qui nous apparaît comme de l'or contient, outre l'or qui y prédomine, des parts de tous les autres ingrédients<ref>Théophraste, dans Simplicius, ''Commentaire sur la Physique'', 27, 2-11 (DK 59 A 41) ; Aristote, ''Physique'', I, 4, 187a36-b7.</ref>.
La tradition doxographique rattache cette thèse aux phénomènes de la nutrition et de la croissance. Comment la chair pourrait-elle provenir du pain et de l'eau que nous consommons, si le pain et l'eau ne contenaient pas déjà de la chair ? La nourriture doit donc contenir, de manière imperceptible, toutes les substances qui composent le corps<ref>Aétius, I, 3, 5 (DK 59 A 46) ; Simplicius, ''Commentaire sur la Physique'', 460, 4-20 (DK 59 A 45). La scholie qui transmet B10 affirme que la semence contient cheveux, ongles, veines, artères, nerfs et os, imperceptibles en raison de leur petitesse et qui se séparent peu à peu au cours de la croissance.</ref>. Curd et Schofield soulignent cependant que la nutrition n'est qu'un cas, particulièrement frappant, d'une question métaphysique plus générale sur le devenir, héritée de Parménide<ref>Curd, 2007, p. 179-180 ; Schofield, 1980, p. 121 et 133-143.</ref>.
Le principe vaut aussi pour les qualités opposées. Selon la scholie à Grégoire de Nazianze, Anaxagore affirmait qu'il y a du noir dans le blanc et du blanc dans le noir, et du léger dans le lourd<ref>Scholie à Grégoire de Nazianze, contexte de DK 59 B 10 (texte et traduction dans Schofield, 1980, p. 135-136). Sextus Empiricus (''Hypotyposes pyrrhoniennes'', I, 33 = DK 59 A 97) rapporte qu'Anaxagore disait la neige noire, puisqu'elle est de l'eau congelée et que l'eau est noire.</ref>. Ces oppositions ne sont donc pas absolues : ce qui nous paraît blanc contient du noir, en proportion trop faible pour être perçue.
Cette doctrine suscite des débats depuis l'Antiquité. Aristote y voit la conséquence de l'observation selon laquelle n'importe quoi naît de n'importe quoi<ref>Aristote, ''Physique'', III, 4, 203a23-33 (DK 59 A 45) ; Schofield, 1980, p. 43-52.</ref>. Les interprètes modernes se divisent sur la nature des « parts » (μοῖραι). Pour les uns, il s'agit de particules infiniment petites (lecture déjà présente chez Lucrèce) ; pour les autres, de proportions ou de concentrations variables d'ingrédients qui se compénètrent sans structure corpusculaire<ref>Lecture particulaire : Lucrèce, ''De la nature'', I, 830-920 (DK 59 A 44) ; Vlastos, 1950 ; W. K. C. Guthrie, ''A History of Greek Philosophy'', vol. II, Cambridge, Cambridge University Press, 1965, p. 289 ; George B. Kerferd, « Anaxagoras and the Concept of Matter before Aristotle », ''Bulletin of the John Rylands Library'', vol. 52, 1969, p. 129-143 ; Sider, 1981. Lecture non particulaire : J. E. Raven, « The Basis of Anaxagoras' Cosmology », ''Classical Quarterly'', n.s. vol. 4, 1954, p. 123-137, ici p. 128-130 ; Schofield, 1980, p. 73-79 ; Jonathan Barnes, ''The Presocratic Philosophers'', Londres, Routledge, 1982, p. 323-326 ; Brad Inwood, « Anaxagoras and Infinite Divisibility », ''Illinois Classical Studies'', vol. 11, 1986, p. 17-33, ici p. 17-18 ; Curd, 2007, p. 183-184 ; Anna Marmodoro, « Anaxagoras's Qualitative Gunk », ''British Journal for the History of Philosophy'', vol. 23, 2015, p. 402-422, et ''Everything in Everything: Anaxagoras's Metaphysics'', New York, Oxford University Press, 2017, chap. 3-4. Voir le recensement de Curd, 2007, p. 183, n. 9-10. Pour d'autres lectures du principe : Margaret E. Reesor, « The Meaning of Anaxagoras », ''Classical Philology'', vol. 55, 1960, p. 1-8 ; Thomas D. Paxson Jr., « The Holism of Anaxagoras », ''Apeiron'', vol. 17, 1983, p. 85-91, pour qui seul le plénum existe par soi ; Adam Drozdek, « Anaxagoras and the Everything in Everything Principle », ''Hermes'', vol. 133, 2005, p. 163-177.</ref>. Quoi qu'il en soit, ce principe forme la clé de voûte de sa physique.
=== Pas de plus petit ni de plus grand ===
Le troisième principe est exposé dans le fragment B3 : « Car du petit il n'y a pas de minimum, mais toujours un plus petit (car ce qui est ne peut pas ne pas être) ; mais du grand aussi il y a toujours un plus grand, et il est égal au petit en quantité ; et, rapportée à elle-même, chaque chose est à la fois grande et petite » (οὔτε γὰρ τοῦ σμικροῦ ἐστί τό γε ἐλάχιστον, ἀλλ' ἔλασσον ἀεί (τὸ γὰρ ἐὸν οὐκ ἔστι τὸ μὴ οὐκ εἶναι), ἀλλὰ καὶ τοῦ μεγάλου ἀεί ἐστι μεῖζον. καὶ ἴσον ἐστὶ τῷ σμικρῷ πλῆθος, πρὸς ἑαυτὸ δὲ ἕκαστόν ἐστι καὶ μέγα καὶ σμικρόν)<ref>Anaxagore, fragment B3, cité par Simplicius, ''Commentaire sur la Physique'', 164, 17-20 (DK 59 B 3). Zeller a proposé de corriger τὸ μή des manuscrits en τομῇ (« par division ») : la parenthèse signifierait alors que ce qui est ne peut cesser d'être par division. Burnet et Sider adoptent la correction ; Curd défend le texte des manuscrits (Curd, 2007, p. 39-40 ; Schofield, 1980, p. 156-157, n. 15).</ref>. Ce principe exclut l'existence d'un plus petit et d'un plus grand, et s'oppose à toute conception atomiste<ref>Leucippe et Démocrite, DK 67-68 ; Aristote, ''Génération et corruption'', I, 2, 315b28-317a2, et I, 8, 325a23-b5.</ref>.
Contrairement à Leucippe et à Démocrite, qui posent des corps insécables (ἄτομα), Anaxagore soutient qu'aucune portion n'est minimale et que toute portion, si petite soit-elle, contient encore toutes choses<ref>Anaxagore, B6 : « Puisque les parts du grand et du petit sont égales en quantité, de cette manière aussi toutes choses seraient en tout » (καὶ ὅτε δὲ ἴσαι μοῖραί εἰσι τοῦ τε μεγάλου καὶ τοῦ σμικροῦ πλῆθος, καὶ οὕτως ἂν εἴη ἐν παντὶ πάντα).</ref>. Cette infinité dans la petitesse se double d'une infinité dans la grandeur : il n'existe pas plus de limite supérieure que de limite inférieure. Cette double infinité pose des problèmes interprétatifs importants, tant aux commentateurs anciens qu'aux modernes<ref>Montgomery Furth, « A “Philosophical Hero”? Anaxagoras and the Eleatics », ''Oxford Studies in Ancient Philosophy'', vol. 9, 1991, p. 95-129 ; David J. Furley, « Anaxagoras, Plato and the Naming of Parts », dans Victor Caston et Daniel W. Graham (éd.), ''Presocratic Philosophy: Essays in Honour of Alexander Mourelatos'', Aldershot, Ashgate, 2002, p. 119-126 ; Marmodoro, 2017, chap. 2-3.</ref>. Miloš Arsenijević, Saša Popović et Miloš Vuletić voient en lui un infinitiste conséquent, qui étend à la physique le principe d'isotropie de l'espace géométrique, si bien que deux parties quelconques du mélange originel sont semblables entre elles<ref>Miloš Arsenijević, Saša Popović et Miloš Vuletić, « Anaxagoras, the Thoroughgoing Infinitist: The Relation between his Teachings on Multitude and on Heterogeneity », ''European Journal of Analytic Philosophy'', vol. 15, 2019, p. 35-70, ici p. 35.</ref>.
Le principe de non-minimum a une conséquence importante : le mélange universel ne pourra jamais être défait. Puisqu'il n'existe pas de plus petite quantité d'un ingrédient, celui-ci ne peut jamais être entièrement extrait d'un mélange ; sa proportion peut diminuer indéfiniment sans jamais devenir nulle<ref>Anaxagore, B6 ; Curd, 2007, p. 182-185. Simplicius commente B3 en ces termes : si tout est en tout et si tout se sépare de tout, alors, de ce qui semble le plus petit, se séparera encore quelque chose de plus petit, et ce qui semble le plus grand s'est séparé de quelque chose de plus grand (''Commentaire sur la Physique'', 164, 20-23).</ref>. Comme le formule Malcolm Schofield à la suite de Colin Strang, la complexité de la composition n'est pas fonction de la taille<ref>Schofield, 1980, p. 69, 79 et 90 ; Strang, 1963, repris dans David J. Furley et R. E. Allen (éd.), ''Studies in Presocratic Philosophy'', vol. II, Londres, Routledge and Kegan Paul, 1975, p. 361-380, ici p. 366 ; cf. Curd, 2007, p. 40 et 50, n. 35.</ref>.
La justification donnée en B3 reprend l'axiome parménidien : « car ce qui est ne peut pas ne pas être ». Si l'on pouvait diviser un ingrédient jusqu'à le faire disparaître, il y aurait passage de l'être au non-être, ce qu'interdit Parménide. Toute division, aussi poussée soit-elle, laisse subsister quelque chose, et ce reste contient encore des parts de tous les ingrédients<ref>Curd, 2007, p. 39-40 et 184-185.</ref>.
=== Le principe de prédominance ===
Du principe « tout est dans tout » découle une difficulté : si chaque chose contient une part de toutes les autres, comment expliquer que nous percevions des objets distincts ? Anaxagore la résout par ce que les commentateurs modernes appellent le « principe de prédominance », expression qui n'est pas la sienne<ref>Schofield, 1980, p. 87 (où il cite les « principles of latency and predominance » de David Furley) et p. 108-111 ; Curd, 2007, p. 188-189 ; Marmodoro (2017, chap. 2) parle de ''preponderance principle''. Les commentateurs néoplatoniciens formulaient déjà les principes du « tout en tout » et de la prédominance (Schofield, 1980, p. 155, n. 52).</ref>. La fin du fragment B12 l'énonce ainsi : « Chaque chose singulière est et était, de la manière la plus manifeste, ce dont elle contient le plus » (ἀλλ' ὅτῳ πλεῖστα ἔνι, ταῦτα ἐνδηλότατα ἓν ἕκαστόν ἐστι καὶ ἦν)<ref>Anaxagore, fragment B12, fin, cité par Simplicius, ''Commentaire sur la Physique'', 156, 13-157, 4 (DK 59 B 12 ; LM 25 D27). La proposition qui précède (νοῦς δὲ πᾶς ὅμοιός ἐστι καὶ ὁ μείζων καὶ ὁ ἐλάττων· ἕτερον δὲ οὐδέν ἐστιν ὅμοιον οὐδενί) est diversement comprise ; A. Wasserstein propose de donner à ὅμοιος le même sens, « homogène », dans ses deux emplois, de sorte que la phrase opposerait l'homogénéité du Noûs au reste des choses (« A Note on Fragment 12 of Anaxagoras », ''Classical Review'', n.s. vol. 10, 1960, p. 4-5).</ref>.
Une chose tire donc son identité apparente des ingrédients qui y prédominent : un morceau d'or nous apparaît comme de l'or parce que l'or y est présent dans une proportion supérieure à celle des autres ingrédients<ref>Théophraste, dans Simplicius, ''Commentaire sur la Physique'', 27, 2-11 (DK 59 A 41) : chaque chose est caractérisée par ce qui prédomine en elle ; Aristote, ''Physique'', I, 4, 187b1-7.</ref>. Cette prédominance n'est jamais absolue, puisque tous les autres ingrédients demeurent présents, mais elle suffit à conférer à l'objet ses caractères perceptibles. Curd propose de la comprendre en termes de concentration ou de densité relative plutôt que de quantité brute<ref>Curd, 2007, p. 188-189.</ref>.
Le principe de prédominance permet de concilier l'ontologie du mélange universel avec l'expérience d'un monde d'objets distincts. Il rend compte aussi du changement : lorsque le pain devient chair, la chair déjà présente dans le pain vient s'ajouter à la chair du corps, tandis que les autres ingrédients se dispersent<ref>Aétius, I, 3, 5 (DK 59 A 46) ; Simplicius, ''Commentaire sur la Physique'', 460, 4-20 (DK 59 A 45) ; Platon, ''Phédon'', 96c-d (DK 59 A 46).</ref>. Le changement apparent se ramène ainsi à une modification des proportions relatives des ingrédients.
Selon Théophraste, Anaxagore soutenait que la perception se fait par les contraires et que toute sensation s'accompagne de douleur, parce que le contact du dissemblable produit une gêne<ref>Théophraste, ''Du sens'', 27-29 (DK 59 A 92) ; Aétius, IV, 9, 16 (DK 59 A 94).</ref>. Les sens ne discernent que les différences marquées de proportion ; Anaxagore reconnaissait leur faiblesse, tout en affirmant que « les choses qui apparaissent sont une vue des choses invisibles » (ὄψις γὰρ τῶν ἀδήλων τὰ φαινόμενα)<ref>Anaxagore, fragments B21 (Sextus Empiricus, ''Contre les mathématiciens'', VII, 90 : « à cause de la faiblesse des sens, nous ne sommes pas capables de discerner le vrai ») et B21a (Sextus Empiricus, ''Contre les mathématiciens'', VII, 140) ; Schofield, 1980, p. 24-25.</ref>.
=== Synthèse : la réponse d'Anaxagore à Parménide ===
Les quatre principes examinés, à savoir le remplacement de la génération et de la destruction par le mélange et la dissociation, le « tout est dans tout », l'absence de plus petit et la prédominance, forment un ensemble cohérent qui constitue la réponse d'Anaxagore à l'exigence parménidienne<ref>Curd, 2007, p. 137-142 et 178-191 ; Schofield, 1980, p. 36-99.</ref>. Anaxagore accepte l'impossibilité du passage de l'être au non-être, mais rejette, ou du moins ne reprend pas, le monisme numérique que la lecture traditionnelle attribue à Parménide, et il admet le mouvement.
En remplaçant la génération et la corruption par le mélange et la dissociation (B17), il rend compte des phénomènes sans enfreindre l'interdit éléate. En posant que tout est dans tout (B6, B11), il explique comment des substances en apparence nouvelles peuvent émerger sans naître de rien : elles étaient déjà présentes, mais imperceptibles. En affirmant qu'il n'y a pas de plus petit (B3, B6), il garantit que le mélange universel ne sera jamais défait. En introduisant le principe de prédominance (B12), il rend compte de la diversité des apparences et de la possibilité de la perception.
Anaxagore entend ainsi préserver la permanence de ce qui est et l'impossibilité du non-être tout en rendant compte de la pluralité et du devenir. Les spécialistes restent partagés sur le degré de son engagement éléate : Curd met l'accent sur la dimension parménidienne du système, Schofield sur sa fidélité au style dogmatique de la cosmologie ionienne<ref>Curd, 2007, p. 141-142 ; Schofield, 1980, p. 26-28 et 142-143 ; Daniel W. Graham, ''Explaining the Cosmos'', 2006.</ref>.
== Les ingrédients primordiaux ==
La question de savoir quels sont exactement les ingrédients (τὰ χρήματα) qui composent l'univers d'Anaxagore divise les commentateurs depuis Aristote<ref>Pour une vue d'ensemble : Schofield, 1980, p. 100-144 ; Curd, 2007, p. 147-150 et 153-191 ; Daniel W. Graham, « Was Anaxagoras a Reductionist? », ''Ancient Philosophy'', vol. 24, 2004, p. 1-18 ; Curd et Sisko, ''SEP'', 2026, § 3.2.</ref>. Anaxagore ne fournit pas de liste systématique, et les fragments conservés mentionnent des entités de natures apparemment différentes. Schofield observe que, malgré le « rôle cardinal » de la doctrine du « tout est dans tout », les fragments montrent qu'Anaxagore accordait au moins autant de place, dans son exposé, au mélange primordial et à l'action cosmogonique du Noûs<ref>Schofield, 1980, p. 100.</ref>.
=== Les opposés ===
Les fragments B4b, B8, B12 et B15 énumèrent plusieurs couples d'opposés : l'humide et le sec (τὸ διερόν καὶ τὸ ξηρόν), le chaud et le froid (τὸ θερμόν καὶ τὸ ψυχρόν), le brillant et l'obscur (τὸ λαμπρόν καὶ τὸ ζοφερόν), le dense et le rare (τὸ πυκνόν καὶ τὸ ἀραιόν)<ref>Anaxagore, B4b, B8, B12, B15. Les fragments B1 et B2 mentionnent l'air et l'éther, non des couples d'opposés.</ref>. Ces opposés jouent un rôle cosmologique central : c'est leur séparation progressive (ἀποκρίνεσθαι) à partir du mélange originel qui produit la diversité des phénomènes<ref>Anaxagore, B12, B13, B15, B16 ; Aristote, ''Physique'', I, 4, 187a25-26 et 187b1-7.</ref>.
Les opposés ne doivent pas être compris comme de simples attributs d'une matière sous-jacente. La distinction entre substance et qualité, et la notion même de « matière » (ὕλη), sont des élaborations aristotéliciennes qu'il est anachronique de projeter sur Anaxagore<ref>Curd, 2007, p. 141, n. 32. Burnet (1930, chap. VI) souligne que le chaud et le froid, le sec et l'humide sont pour Anaxagore des « choses » (χρήματα).</ref>. Le chaud n'est pas une propriété d'une matière indéterminée, mais une réalité présente en plus ou moins grande proportion dans un mélange. F. M. Cornford parle à leur propos de « choses-qualités » (''quality-things'')<ref>F. M. Cornford, « Anaxagoras' Theory of Matter », ''Classical Quarterly'', vol. 24, 1930, p. 14-30 et 83-95, ici p. 84 (cité par Teodorsson, 1982, p. 35).</ref> ; Gregory Vlastos les décrit comme des puissances dont la combinaison en certaines proportions produit les substances naturelles<ref>Vlastos, 1950, repris dans ''Studies in Greek Philosophy'', vol. I, éd. Daniel W. Graham, Princeton, Princeton University Press, 1995, p. 303-327, ici p. 322 (cité par Curd, 2007, p. 168, n. 31).</ref>.
=== L'interprétation « austère » : les opposés seuls ===
Paul Tannery fut l'un des premiers à contester l'interprétation aristotélicienne selon laquelle Anaxagore aurait posé des « homéomères » élémentaires (chair, os, etc.) et à faire des opposés les véritables constituants<ref>Paul Tannery, « La théorie de la matière d'Anaxagore », ''Revue philosophique'', vol. 22, 1886, p. 255-274 ; repris dans ''Pour l'histoire de la science hellène'', Paris, Alcan, 1887 (Burnet renvoie à la p. 283 sq.). Voir Teodorsson, 1982, p. 30.</ref>. John Burnet adopta une position voisine ; il remarquait que, même après la définition de la notion de qualité (ποιότης), cette manière de penser les opposés comme des choses avait survécu, et s'appuyait sur Galien, pour qui les qualités sont éternelles chez Anaxagore<ref>Burnet, 1930, chap. VI, section « The Portions », qui cite Galien, ''Des facultés naturelles'', I, 2.</ref>.
Cette interprétation présente plusieurs avantages. Elle s'appuie étroitement sur les fragments plutôt que sur les reconstructions d'Aristote, qui écrivait environ un siècle après Anaxagore avec ses propres préoccupations. Elle rend compte du rôle cosmologique des opposés dans les fragments B12, B15 et B16<ref>Schofield, 1980, p. 107-121 ; Curd, 2007, p. 156-157 et 163-166.</ref>. Elle a été défendue, sous des formes diverses, par Tannery, Burnet, Cornford, Vlastos, Schofield, Inwood, Sedley et Marmodoro<ref>Curd, 2007, p. 156, n. 8, et p. 164, n. 21 ; Marmodoro, 2017, chap. 1, § 1.1, n. 4 ; Curd et Sisko, ''SEP'', 2026, § 3.2 ; David Sedley, ''Creationism and Its Critics in Antiquity'', Berkeley, University of California Press, 2007. Schofield (1980, p. 114-116 et 132-133) distingue des ingrédients fondamentaux (les opposés) et des ingrédients dérivés (air, terre, eau, semences) ; il a ensuite nuancé sa position (Curd, 2007, p. 156, n. 8). Marmodoro comprend les opposés comme des puissances et tient les substances pour réductibles aux opposés.</ref>.
=== L'interprétation « expansive » et le témoignage d'Aristote ===
La question devient plus complexe lorsqu'on prend en compte les témoignages indirects. Aristote attribue à Anaxagore une doctrine des « homéomères » (τὰ ὁμοιομερῆ), substances dont les parties portent le même nom que le tout, comme la chair, l'os ou la moelle<ref>Aristote, ''Génération et corruption'', I, 1, 314a18-20 (DK 59 A 46) ; ''Du ciel'', III, 3, 302a28-b4 (DK 59 A 43) ; ''Physique'', I, 4, 187a25-26.</ref>.
Le terme d'homéomère n'apparaît pourtant dans aucun fragment. Il appartient au vocabulaire d'Aristote, pour qui les homéomères constituent un niveau intermédiaire entre les éléments et les organes<ref>Aristote, ''Parties des animaux'', II, 1, 646a12-24 ; Curd, 2007, p. 147-150. Burnet (1930, chap. VI) jugeait étrange, si Anaxagore avait employé le terme, que Simplicius ne cite aucun fragment qui le contienne.</ref>. Curd montre en outre qu'Aristote ne prête pas à Anaxagore un « principe d'homéomérie » au sens strict, et qu'il se trompe en faisant de l'air et du feu des mélanges d'homéomères<ref>Curd, 2007, p. 148-150.</ref>. D'autres interprètes réhabilitent au contraire la notion : William E. Mann en fait le centre de son interprétation, et John Sisko juge l'homéomérie compatible avec les autres principes d'Anaxagore et probablement authentique<ref>William E. Mann, « Anaxagoras and the ''Homoiomerē'' », ''Phronesis'', vol. 25, 1980, p. 228-249, ici p. 228 et 231-233 ; John E. Sisko, « Anaxagoras on Matter, Motion, and Multiple Worlds », ''Philosophy Compass'', vol. 5, 2010, p. 443-454, ici p. 443.</ref>.
Une lecture « expansive » soutient néanmoins que tout ce qui apparaît dans le monde sensible se trouvait déjà dans le mélange originel. Elle a été défendue, sous des formes diverses, par Strang, Stokes, Guthrie, Barnes, Furth et Graham ; Peck en proposait une version qui écartait les substances inorganiques<ref>Curd, 2007, p. 154-156 et n. 3-4, p. 158-159, n. 12 ; Curd et Sisko, ''SEP'', 2026, § 3.2 ; Arthur L. Peck, « Anaxagoras: Predication as a Problem in Physics », ''Classical Quarterly'', vol. 25, 1931, p. 27-37 et 112-120, ici p. 30, où les substances organiques sont tenues pour élémentaires et les substances inorganiques pour dérivées. Kerferd (1969) admet une forme d'homéomérie compatible avec le mélange universel (Curd et Sisko, § 3.7).</ref>. Gershenson et Greenberg proposent de leur côté une lecture particulaire dans laquelle les tissus organiques, conçus comme des « molécules » infinitésimales, sont les éléments de toute matière<ref>Daniel E. Gershenson et Daniel A. Greenberg, ''Anaxagoras and the Birth of Scientific Method'', New York, Blaisdell, 1964, p. 9-12 et 14-22.</ref>.
=== L'interprétation modérée ===
Une troisième voie a été proposée par Patricia Curd, dont la position est, selon elle, proche de celle de William E. Mann, sauf pour les semences, et a été reprise par John Sisko<ref>Curd, 2007, p. 157-171 et n. 10 ; Mann, 1980 ; Curd et Sisko, ''SEP'', 2026, § 3.2.</ref>. Selon cette lecture, les ingrédients comprennent les opposés et diverses substances : terre, air, éther, eau, feu, métaux, chair, sang, os. En revanche, les plantes, les animaux et leurs organes ne sont pas des ingrédients primordiaux, mais des composés temporaires, des « artefacts naturels » selon l'expression de Curd<ref>Curd, 2007, p. 157-163 et 170-171.</ref>.
Cette lecture tient compte à la fois des fragments et des témoignages d'Aristote. Elle reconnaît le rôle cosmologique des opposés (B12, B15) tout en admettant que d'autres substances figurent au même niveau dans le mélange : l'air et l'éther recouvrent tout avant même la rotation (B1), et B4b mentionne la terre et les semences à côté des opposés<ref>Curd, 2007, p. 166-167.</ref>.
=== Les semences (σπέρματα) ===
Les « semences » (σπέρματα), mentionnées dans les fragments B4a et B4b, ajoutent une difficulté supplémentaire. Selon B4b, avant toute séparation, aucune couleur n'était manifeste, car le mélange de toutes choses l'empêchait, « la terre étant présente en abondance et des semences illimitées en nombre, en rien semblables les unes aux autres » (καὶ γῆς πολλῆς ἐνεούσης καὶ σπερμάτων ἀπείρων πλῆθος οὐδὲν ἐοικότων ἀλλήλοις)<ref>Anaxagore, fragment B4b, cité par Simplicius, ''Commentaire sur la Physique'', 34, 21-26 (DK 59 B 4b). Sur la division de DK B4 en B4a et B4b, voir Curd, 2007, p. 42.</ref>. Le fragment B4a déclare : « Puisqu'il en est ainsi, il faut penser qu'il y a beaucoup de choses de toutes sortes dans toutes les choses qui se composent, et des semences de toutes choses, ayant des formes, des couleurs et des saveurs de toute espèce »<ref>Anaxagore, fragment B4a, cité par Simplicius, ''Commentaire sur la Physique'', 34, 29-35, 9 (DK 59 B 4a). Simplicius précise que le passage se trouvait peu après le début du livre.</ref>.
La nature de ces semences est débattue. Gregory Vlastos voyait dans σπέρμα un terme technique désignant un agrégat de tous les ingrédients dans lequel l'un d'eux prédomine ; G. E. R. Lloyd et W. K. C. Guthrie ont adopté des positions voisines<ref>Vlastos, 1950, repris dans Furley et Allen (éd.), 1975, vol. II, p. 323-353, ici p. 324 ; G. E. R. Lloyd, ''Polarity and Analogy'', Cambridge, Cambridge University Press, 1966, p. 246-247 ; Guthrie, 1965, p. 298-300. Voir Schofield, 1980, p. 123 et n. 38-39.</ref>. Une lecture aujourd'hui répandue, défendue par David Furley, Malcolm Schofield, Patricia Curd, David Sedley et Anna Marmodoro, comprend les semences au sens biologique ordinaire, comme les germes à partir desquels croissent plantes et animaux<ref>David J. Furley, « Anaxagoras in Response to Parmenides », ''Canadian Journal of Philosophy'', suppl. vol. 2, 1976, p. 61-85 ; Schofield, 1980, p. 123-126 ; Curd, 2007, p. 171-177 ; Sedley, 2007 ; Marmodoro, 2017, chap. 5.</ref>. Marmodoro précise ce rôle : les semences sont des « cadres » physiques auxquels s'ajoutent les opposés dispersés par le tourbillon, et elles possèdent une efficacité causale propre, celle de « puissances de vie » qui règlent le développement structurel des organismes<ref>Marmodoro, 2017, p. 7 et 147-153.</ref>. D'autres en font des « homoncules » préformés de tous les organismes à venir (Eric Lewis), ou encore des « points de croissance » dont le développement serait réglé d'avance par le Noûs (Teodorsson)<ref>Eric Lewis, « Anaxagoras and the Seeds of a Physical Theory », ''Apeiron'', vol. 33, 2000, p. 1-23, ici p. 1 et 16-19 ; Teodorsson, 1982, p. 80-91, qui recense aussi les interprétations antérieures des semences (p. 45 et suiv.). David Sider juge l'hypothèse de Teodorsson ingénieuse mais insuffisamment argumentée (compte rendu de Teodorsson, 1982, ''Classical Journal'', vol. 80, 1984, p. 71-73, ici p. 72). Critique de la lecture en homoncules chez Mann, 1980, p. 236, et Curd, 2007, p. 172-175.</ref>. Aristote, pour sa part, semble avoir rangé sous ce terme les homéomères eux-mêmes<ref>Aristote, ''Du ciel'', III, 3, 302a28-b4 (DK 59 A 43) ; ''Génération et corruption'', I, 1, 314a28-b1 ; Curd, 2007, p. 151-152 ; Schofield, 1980, p. 128-132.</ref>.
La lecture biologique ne fait pas violence au langage : le mot est employé dans son sens ordinaire, dans un contexte (B4a) où il est question d'hommes, d'animaux et de cultures. Elle s'accorde avec d'autres témoignages. Théophraste rapporte qu'Anaxagore disait l'air porteur de semences de toutes choses, qui, entraînées avec l'eau de pluie, engendrent les plantes<ref>Théophraste, ''Recherches sur les plantes'', III, 1, 4 (DK 59 A 117 ; cf. ''Causes des plantes'', I, 5, 2) ; Irénée, ''Contre les hérésies'', II, 14, 2 (DK 59 A 113) ; Schofield, 1980, p. 124-126.</ref>. Diogène Laërce rapporte que les animaux naquirent d'abord de l'humide, du chaud et du terreux, puis les uns des autres<ref>Diogène Laërce, II, 9 (DK 59 A 1) ; cf. Hippolyte, ''Réfutation de toutes les hérésies'', I, 8, 12 (DK 59 A 42) : les animaux naquirent d'abord dans l'humide, puis les uns des autres.</ref>.
== L'état originel : tout ensemble ==
Le traité d'Anaxagore s'ouvrait sur l'une des déclarations les plus célèbres de la philosophie présocratique : « Toutes choses étaient ensemble » (ὁμοῦ χρήματα πάντα ἦν)<ref>Anaxagore, fragment B1, cité par Simplicius, ''Commentaire sur la Physique'', 155, 26-30 (DK 59 B 1 ; LM 25 D9). Diels et Kranz impriment ὁμοῦ πάντα χρήματα ἦν ; Wolfgang Rösler a montré que les deux citations les plus complètes de Simplicius (''Commentaire sur la Physique'', 155, 26-30 ; ''Commentaire sur le Ciel'', 608, 21-23) portent ὁμοῦ χρήματα πάντα ἦν, ordre retenu par Sider et par Curd (Rösler, « ΟΜΟΥ ΧΡΗΜΑΤΑ ΠΑΝΤΑ ΗΝ », ''Hermes'', vol. 99, 1971, p. 246-248 ; Curd, 2007, p. 33 ; Schofield, 1980, p. 151, n. 1). La phrase citée par Diogène Laërce (II, 6), « Toutes choses étaient ensemble ; puis l'Intellect vint et les ordonna », est un résumé et non un fragment (Burnet, 1930, chap. VI).</ref>. Cette formule d'ouverture exprime la thèse cosmogonique d'Anaxagore sur l'état primordial de l'univers ; Schofield souligne que le livre commençait sans préambule personnel, en exposant d'emblée ce thème<ref>Schofield, 1980, p. 36-40.</ref>.
La formulation d'Anaxagore répond à Parménide<ref>Owen, 1966, repris dans Mourelatos (éd.), 1974, p. 276-277 ; Schofield, 1980, p. 64.</ref>. Là où Parménide affirmait que l'être « est maintenant tout entier ensemble, un, continu » (νῦν ἔστιν ὁμοῦ πᾶν, ἕν, συνεχές)<ref>Parménide, B 8, 5-6 (DK 28 B 8).</ref>, Anaxagore proclame que « toutes choses étaient ensemble ». À l'unité, à la continuité et au présent intemporel de l'être parménidien, il oppose la pluralité, la divisibilité illimitée, un état passé et un devenir cosmogonique<ref>Schofield, 1980, p. 64-65 ; Curd, 2007, p. 153-154.</ref>.
=== La description du mélange originel ===
Le fragment B1, que Simplicius dit placé au début du premier livre, décrit cet état primordial :
<blockquote>Toutes choses étaient ensemble, illimitées en quantité et en petitesse, car le petit aussi était illimité. Et toutes choses étant ensemble, rien n'était manifeste en raison de la petitesse ; car l'air et l'éther recouvraient toutes choses, étant l'un et l'autre illimités : ce sont eux, en effet, les plus grands dans l'ensemble des choses, et en quantité et en grandeur.<ref>Anaxagore, fragment B1 (DK 59 B 1). Le verbe κατεῖχεν est rendu ici par « recouvraient » ; Burnet le traduisait par « prédominaient », et David Sider a défendu cette lecture : comme toute chose se caractérise par ce qui prédomine en elle (B12), le mélange originel aurait présenté l'aspect de l'air et de l'éther, qui y prédominaient (« A Note on Anaxagoras, Fr. 1 », ''Archiv für Geschichte der Philosophie'', vol. 55, 1973, p. 249-251). Gigon jugeait cette traduction difficile sur le plan lexical.</ref></blockquote>
Quatre caractéristiques du mélange originel y sont énoncées : toutes choses étaient ensemble ; elles étaient illimitées en quantité et en petitesse ; rien n'était manifeste en raison de la petitesse ; l'air et l'éther recouvraient toutes choses.
L'expression « illimitées en quantité et en petitesse » a donné lieu à deux lectures principales, que Schofield a nommées interprétation « particulaire » et interprétation « proportionnelle »<ref>Schofield, 1980, p. 70-79.</ref>. La première comprend que le mélange originel contenait une infinité de petites particules distinctes<ref>Vlastos, 1950 ; Guthrie, 1965, p. 289 ; Kerferd, 1969 ; Sider, 1981 (voir Curd, 2007, p. 183, n. 9 ; Curd et Sisko, ''SEP'', 2026, § 3.3).</ref>. La seconde refuse de concevoir le mélange comme une collection de particules : chaque ingrédient y est présent en une proportion aussi faible qu'on voudra par rapport à l'ensemble<ref>Schofield, 1980, p. 73-79 ; Barnes, 1982, p. 323-326 ; Inwood, 1986, p. 17-18. Curd (2007, p. 181-187) propose un modèle de « densités » : les ingrédients, comparables à des liquides ou à des pâtes, sont présents partout à des concentrations variables.</ref>. Le débat n'est pas tranché<ref>Pour une discussion d'ensemble, voir Curd, 2007, p. 181-191, et Marmodoro, 2017, chap. 4 (p. 105-127).</ref>.
=== L'imperceptibilité du mélange ===
La troisième caractéristique du mélange originel est son indistinction : « rien n'était manifeste » (οὐδὲν ἔνδηλον ἦν). Anaxagore l'explique par deux facteurs : la petitesse (ὑπὸ σμικρότητος) et le fait que « l'air et l'éther recouvraient toutes choses » (πάντα γὰρ ἀήρ τε καὶ αἰθὴρ κατεῖχεν).
L'air (ἀήρ) désigne ici, selon l'usage ionien ancien, une brume sombre, humide, froide et dense ; l'éther (αἰθήρ), la substance brillante, chaude, sèche et rare, qu'Aristote dit identifiée au feu par Anaxagore<ref>Théophraste, ''Du sens'', 59 (DK 59 A 70) : le rare et le fin sont chauds, le dense et l'épais froids, comme Anaxagore définit l'éther et l'air ; Aristote, ''Du ciel'', I, 3, 270b24-25 (DK 59 A 73), et III, 3, 302b4 ; Schofield, 1980, p. 71. Peter Kingsley souligne qu'il s'agit d'une interprétation d'Aristote, plus prudent dans les ''Météorologiques'' : chez Anaxagore, l'éther serait plutôt un air sec qui tend vers le haut, dont le feu dérive comme l'eau et la terre dérivent de l'air humide (« Notes on Air: Four Questions of Meaning in Empedocles and Anaxagoras », ''Classical Quarterly'', n.s. vol. 45, 1995, p. 26-29, ici p. 28-29).</ref>. Ces deux substances, dit B1, sont les plus grandes dans l'ensemble des choses, en quantité et en grandeur : elles prédominaient dans le mélange originel, à la manière d'un brouillard qui recouvre tout<ref>Curd, 2007, p. 178 et n. 1 ; Schofield, 1980, p. 155-156, n. 5-6.</ref>.
Le fragment B4b confirme cette description :
<blockquote>Mais avant que ces choses ne fussent séparées, toutes étant ensemble, aucune couleur n'était manifeste ; car le mélange de toutes choses l'empêchait, celui de l'humide et du sec, du chaud et du froid, du brillant et de l'obscur, la terre étant présente en abondance et des semences illimitées en nombre, en rien semblables les unes aux autres.<ref>Anaxagore, fragment B4b (DK 59 B 4b).</ref></blockquote>
L'indistinction du mélange originel ne tenait donc pas à l'absence des ingrédients, mais à leur mélange si intime qu'aucun ne pouvait se manifester. Curd souligne qu'il s'agit d'un contrefactuel : aucun observateur n'était présent, mais un observateur n'aurait rien pu y distinguer<ref>Curd, 2007, p. 46.</ref>.
=== L'immobilité originelle ===
Plusieurs témoignages rapportent qu'avant l'intervention du Noûs, le mélange était au repos depuis un temps illimité<ref>Aristote, ''Physique'', VIII, 1, 250b24-26 ; Simplicius, ''Commentaire sur la Physique'', 1121, 21 (DK 59 A 64) ; Aétius, I, 7, 5 (DK 59 A 48).</ref>. Cette immobilité pose une difficulté : si le mélange était au repos, qu'est-ce qui a pu le mettre en mouvement ? Anaxagore répond en posant le Noûs (Νοῦς, Intellect), distinct de tous les ingrédients, qui possède le pouvoir d'initier le mouvement<ref>Anaxagore, B12 et B13.</ref>. Eudème reprochait déjà à Anaxagore de faire commencer le mouvement à un moment donné sans dire s'il cesserait un jour<ref>Simplicius, ''Commentaire sur la Physique'', 1185, 9 (DK 59 A 59). Une colonne d'un papyrus d'Herculanum (Philodème) attribue au contraire à Anaxagore l'idée d'un mouvement éternel ; voir Christian Vassallo, ''The Presocratics at Herculaneum'', Berlin et Boston, De Gruyter, 2021, et Curd et Sisko, ''SEP'', 2026, § 4.4.</ref>.
=== L'étendue du mélange originel ===
Le fragment B1 affirme que l'air et l'éther « étaient l'un et l'autre illimités » (ἀμφότερα ἄπειρα ἐόντα), et le fragment B2 précise que « l'air et l'éther se séparent de la masse environnante, et la masse environnante est illimitée en quantité »<ref>Anaxagore, fragment B2 (DK 59 B 2).</ref>. Ces fragments ont généralement été compris comme impliquant que le mélange originel était spatialement illimité, le sens exact d'ἄπειρον (« illimité » ou « indéfini ») restant discuté<ref>Aristote, ''Physique'', III, 4, 203a19-33 (DK 59 A 45) ; III, 5, 205b1-5 (DK 59 A 50), où Aristote critique l'idée que l'illimité se fixe lui-même en place ; sur les sens d'ἄπειρον, voir Curd et Sisko, ''SEP'', 2026, § 3.2 et 3.5.</ref>. Le fragment B12 indique que la révolution a commencé à partir d'une petite région, qu'elle s'étend maintenant davantage et qu'elle s'étendra davantage encore<ref>Anaxagore, B12 (DK 59 B 12).</ref>. Le processus cosmogonique n'a donc pas encore affecté la totalité du mélange : la description de B1 vaut toujours pour les régions que la rotation n'a pas atteintes<ref>Curd, 2007, p. 207-208.</ref>. Cette conception d'un univers partiellement ordonné, dont la formation se poursuit à la périphérie, compte parmi les idées les plus originales d'Anaxagore.
== Le Noûs : l'Intellect cosmique ==
Le fragment B12, le plus long des fragments conservés, est presque entièrement consacré au Noûs (Νοῦς, « Intellect » ou « Esprit »). Schofield y voit l'un des passages les plus puissants de toute la prose grecque, par son intensité et sa lente grandeur<ref>Schofield, 1980, p. 4 ; sur le style de « prédication solennelle », voir Deichgräber, 1933, et Schofield, 1980, p. 6-9.</ref>. Anaxagore y expose la nature du Noûs et son rôle dans la formation du monde. La tradition a souvent tenu cette doctrine pour son apport le plus original ; Diogène Laërce et Plutarque rapportent qu'on le surnommait lui-même « Noûs », surnom que Timon tourne en dérision dans ses ''Silles''<ref>Diogène Laërce, II, 6 (DK 59 A 1), qui cite Timon ; Plutarque, ''Vie de Périclès'', 4 (DK 59 A 15) ; Curd, 2007, p. 192. Burnet (1930, chap. VI) jugeait au contraire que l'originalité d'Anaxagore tenait davantage à sa théorie de la substance qu'à celle du Noûs. Walter Bröcker constatait ce partage : Zeller et Capelle voyaient l'apport d'Anaxagore dans sa doctrine de l'intellect, Tannery et Burnet dans sa théorie de la matière (« Die Lehre des Anaxagoras », ''Kant-Studien'', vol. 42, 1942-1943, p. 176-189, ici p. 176).</ref>.
Deux points doivent être distingués. Le Noûs est d'abord la source du mouvement : il initie et contrôle la révolution qui produit la séparation des ingrédients à partir du mélange originel. Il est aussi un intellect : il connaît, discerne et ordonne. En revanche, aucun fragment conservé n'affirme que l'Intellect dispose les choses de la manière qui est la meilleure. C'est précisément ce que Socrate, dans le ''Phédon'', aurait voulu trouver chez Anaxagore, et dont il déplore l'absence<ref>Platon, ''Phédon'', 97b-98c (DK 59 A 47) ; Curd, 2007, p. 144 et 192.</ref>. L'identification du Noûs à une cause finale organisant le monde en vue du bien doit donc beaucoup à la lecture de Platon, puis d'Aristote.
Appliquer aux présocratiques la distinction aristotélicienne des quatre causes (matérielle, formelle, efficiente, finale) est par ailleurs anachronique : cette classification est une construction d'Aristote, élaborée pour situer ses prédécesseurs par rapport à sa propre doctrine<ref>Patricia Curd, « Presocratic Philosophy », ''Stanford Encyclopedia of Philosophy'', 2007, révision substantielle 2020, § 2 ; Curd, 2007, p. 141, n. 32, et p. 198, où elle qualifie elle-même d'anachronique l'expression de « cause efficiente ».</ref>. Le Noûs ne se laisse ranger entièrement dans aucune de ces catégories. La question de savoir s'il poursuit des fins, et en quel sens, fait l'objet d'un débat qui est présenté plus loin.
=== La séparation du Noûs d'avec toutes choses ===
Le fragment B12 s'ouvre par une affirmation qui constitue la thèse centrale d'Anaxagore concernant le Noûs :
<blockquote>Les autres choses ont part à toute chose, mais l'Intellect est illimité et maître de lui-même, il n'est mêlé à aucune chose, mais il est seul, lui-même par lui-même.<ref>Anaxagore, fragment B12, cité par Simplicius, ''Commentaire sur la Physique'', 156, 13-15 (DK 59 B 12). Simplicius cite la première proposition en 164, 24-25.</ref></blockquote>
Le Noûs fait ainsi exception au principe du mélange universel : alors que tous les ingrédients contiennent des parts de tous les autres, lui seul demeure pur et séparé<ref>Anaxagore, B11 (DK 59 B 11).</ref>. Le fragment B14 affirme pourtant qu'il est là où sont toutes les autres choses : dans la masse environnante, dans ce qui s'est adjoint et dans ce qui s'est séparé<ref>Anaxagore, B14, cité par Simplicius, ''Commentaire sur la Physique'', 157, 5-7 (DK 59 B 14). Le début du fragment est altéré ; voir David Sider, « Anaxagoras Fr. 14 DK », ''Hermes'', vol. 102, 1974, p. 365-367.</ref>. Trois attributs lui sont d'abord prédiqués : il est « illimité » (ἄπειρον), « maître de lui-même » (αὐτοκρατές) et « mêlé à aucune chose ». Anaxagore justifie cette séparation par un argument :
<blockquote>Car s'il n'était pas par lui-même, mais s'il était mêlé à quelque autre chose, il aurait part à toutes choses, s'il était mêlé à l'une d'elles ; car en toute chose il y a une part de toute chose, comme je l'ai dit auparavant. Et les choses mêlées à lui l'empêcheraient, de sorte qu'il ne dominerait aucune chose comme il le fait, étant seul par lui-même.<ref>Anaxagore, B12, cité par Simplicius, ''Commentaire sur la Physique'', 156, 15-20.</ref></blockquote>
Il s'agit d'une preuve indirecte : si le Noûs était mêlé à quoi que ce soit, il serait mêlé à tout ; les choses mêlées à lui l'empêcheraient d'exercer son pouvoir ; or il l'exerce ; il n'est donc mêlé à rien<ref>Schofield, 1980, p. 7 et 19, qui voit dans l'usage de cette forme d'argument un possible indice d'influence éléate (p. 146, n. 15).</ref>. Anaxagore n'explique pas pourquoi le mélange entraverait l'action du Noûs, et plusieurs explications ont été proposées<ref>Curd, 2007, p. 58-59 et 200-201 ; Schofield, 1980, p. 147-148, n. 39.</ref>.
=== Les attributs du Noûs ===
Après avoir établi la séparation du Noûs, Anaxagore énonce une série d'attributs dans le style de la prédication solennelle, qu'Eduard Norden et Karl Deichgräber ont rapproché de l'hymne religieux<ref>Deichgräber, 1933, p. 347-361 ; Eduard Norden, ''Agnostos Theos'', Leipzig, Teubner, 1913 (4{{e}} tirage, Stuttgart, 1956), p. 143-176 ; Schofield, 1980, p. 6-9 et 12.</ref> :
<blockquote>Car il est la plus fine de toutes les choses et la plus pure ; il détient tout discernement (γνώμη) sur toute chose et il a la plus grande force ; et toutes les choses qui ont une âme, les plus grandes comme les plus petites, l'Intellect les domine.<ref>Anaxagore, B12, cité par Simplicius, ''Commentaire sur la Physique'', 156, 20-24.</ref></blockquote>
Le premier attribut, qui qualifie le Noûs de « plus fin » (λεπτότατον) et « plus pur » (καθαρώτατον), a reçu des interprétations divergentes, que Schofield ramène à trois<ref>Schofield, 1980, p. 11-12.</ref>. Pour les uns, il s'agit d'une substance matérielle d'une extrême finesse : Anaxagore emploie probablement λεπτός en un sens physique à propos de l'eau de mer, et il parle d'un intellect « plus grand » ou « plus petit »<ref>Aétius, III, 16, 2 (DK 59 A 90) ; Burnet, 1930, p. 268 ; J. E. Raven, « The Basis of Anaxagoras' Cosmology », ''Classical Quarterly'', n.s. vol. 4, 1954, p. 123-137, ici p. 134-135 ; Barnes, 1982, p. 406-409 ; Sider, 1981. Voir Schofield, 1980, p. 147, n. 28, et Curd, 2007, p. 59, n. 50.</ref>. Pour d'autres, ces termes visent à exprimer l'incorporéité du Noûs<ref>Guthrie, 1965, p. 276-278 ; Curd, 2007, p. 59 et 200 ; Gershenson et Greenberg, 1964, p. 32-33, qui font d'Anaxagore le premier penseur grec à introduire une entité substantielle incorporelle.</ref>. Une troisième lecture, celle d'Aristote, à laquelle Schofield se rallie, comprend que le Noûs n'a aucun caractère propre qui le rendrait semblable aux choses qu'il connaît et domine<ref>Aristote, ''De l'âme'', III, 4, 429a18-24 (DK 59 A 100) ; Schofield, 1980, p. 11-12.</ref>. On a longtemps soutenu que la notion d'incorporéité n'était pas encore disponible à l'époque d'Anaxagore ; Zeller jugeait qu'il avait voulu parler d'un incorporel sans y parvenir<ref>Burnet, 1930, chap. VI, qui rapporte la position de Zeller ; Raven, 1954, p. 130, pour qui aucun présocratique n'avait encore saisi l'existence de l'incorporel.</ref>. Curd rappelle toutefois que Mélissos refuse déjà un corps à l'Un<ref>Mélissos, DK 30 B 9 ; Curd, 2007, p. 59.</ref>.
Le deuxième attribut concerne la connaissance : le Noûs « détient tout discernement sur toute chose ». Le troisième concerne la puissance : il « a la plus grande force ». Anaxagore suggère le lien entre les deux par une assonance, ἴσχει (« il détient ») et ἰσχύει (« il a de la force »)<ref>Schofield, 1980, p. 15 et p. 147, n. 36 ; Curd, 2007, p. 60. James Lesher souligne que γνώμη unit connaissance et détermination : le Noûs juge et décide de toutes choses, mais à partir d'un savoir (« Mind's Knowledge and Powers of Control in Anaxagoras DK B12 », ''Phronesis'', vol. 40, 1995, p. 125-142, ici p. 138-141).</ref>. Le quatrième attribut, le contrôle exercé sur tous les êtres animés, s'accorde avec B11, selon lequel l'Intellect est présent dans certaines choses. Schofield montre que tout le passage peut se lire aussi bien comme une description d'un Intellect suprême que comme une thèse sur l'intellect en général, et que cette ambiguïté est probablement inhérente au texte<ref>Schofield, 1980, p. 10-22 ; Curd, 2007, p. 60-61.</ref>.
=== Le rôle cosmogonique du Noûs ===
Après avoir décrit la nature du Noûs, Anaxagore expose son rôle dans la cosmogonie :
<blockquote>Et l'Intellect a dominé la révolution entière, de sorte qu'elle a commencé à tourner au commencement. Elle a d'abord commencé à tourner à partir d'une petite région, mais elle tourne sur une région plus grande, et elle tournera sur une région plus grande encore. Et les choses qui se mêlent, qui se séparent et qui se dissocient, l'Intellect les a toutes connues. Et celles qui devaient être, celles qui étaient et ne sont plus, celles qui sont maintenant et celles qui seront, l'Intellect les a toutes ordonnées, ainsi que cette révolution dans laquelle tournent maintenant les astres, le soleil, la lune, l'air et l'éther qui se séparent.<ref>Anaxagore, B12, cité par Simplicius, ''Commentaire sur la Physique'', 156, 24-157, 2.</ref></blockquote>
Ce passage affirme trois choses. L'Intellect initie une révolution (περιχώρησις) dans un mélange jusqu'alors immobile. Ce mouvement, commencé dans une petite région, s'étend progressivement et continue de s'étendre : la cosmogonie n'est pas un événement révolu, mais un processus qui se poursuit à la périphérie du mélange illimité. Enfin, c'est ce mouvement qui produit la séparation (ἀπόκρισις) et la dissociation (διάκρισις) des ingrédients, d'où naît le cosmos ordonné que nous observons<ref>Anaxagore, B12 et B13 ; Simplicius, ''Commentaire sur la Physique'', 300, 27-301, 1 ; Aristote, ''Physique'', VIII, 1, 250b24-26.</ref>.
Le mécanisme de la séparation est d'ordre physique : la rotation, par sa vitesse et sa force, rassemble au centre les ingrédients denses, humides, froids et obscurs, et repousse vers la périphérie les ingrédients rares, chauds, secs et brillants<ref>Anaxagore, B9, B12, B15 ; Aristote, ''Du ciel'', II, 13, 295a9-14 (DK 59 A 88), qui rapporte que, dans les tourbillons, les corps les plus lourds se portent vers le centre ; Curd, 2007, p. 207-208.</ref>. Le Noûs n'intervient donc pas dans chaque détail : il initie et gouverne la rotation, qui produit ensuite la séparation et la recomposition des ingrédients. C'est pourquoi Platon et Aristote pourront reprocher à Anaxagore de ne plus recourir à l'Intellect dans l'explication des phénomènes particuliers. Simplicius répondait déjà que le Noûs demeure la cause première, puisque la génération est séparation, que la séparation résulte du mouvement et que le Noûs est cause du mouvement<ref>Simplicius, ''Commentaire sur la Physique'', 300, 27-31 ; Curd, 2007, p. 202-203.</ref>.
Le texte affirme aussi que « l'Intellect a ordonné » (διεκόσμησε νοῦς) toutes choses, passées, présentes et futures. Le verbe διακοσμεῖν signifie « disposer », « mettre en ordre ». Le Noûs connaît et dispose ; il est à la fois intelligent et ordonnateur. Mais aucun fragment n'explique cet ordre par le meilleur, et la portée téléologique de la doctrine reste débattue. De nombreux interprètes contemporains attribuent au Noûs une forme de téléologie : une téléologie « mince », qui lie sa connaissance à son action (Lesher, Curd, Pinto), ou « épaisse », qui lui prête des buts déterminés, comme la connaissance par discernement des ingrédients (André Laks) ou la production du monde le plus favorable à la vie humaine (David Sedley)<ref>Curd et Sisko, ''SEP'', 2026, § 4.2 ; Lesher, 1995 ; Curd, 2007, p. 194 et 204-205 ; Rhodes Pinto, « ''Nous'', Motion, and Teleology in Anaxagoras », ''Oxford Studies in Ancient Philosophy'', vol. 52, 2017, p. 1-32 ; André Laks, « Mind's Crisis: On Anaxagoras' ''Nous'' », ''Southern Journal of Philosophy'', vol. 31, suppl., 1993, p. 19-38 ; Sedley, 2007. Anna Marmodoro voit en Anaxagore le premier à proposer une approche téléologique de la cosmologie, le Noûs développant le monde selon sa conception de l'ordre à partir de ce qui est donné (Marmodoro, 2017, p. 129-130).</ref>. D'autres comprennent le Noûs comme une loi de la nature plutôt que comme un agent qui poursuit des fins<ref>Sider, 1981, selon Curd et Sisko, ''SEP'', 2026, § 4.1-4.2 ; Gershenson et Greenberg, 1964, p. 25 ; John E. Sisko, « Anaxagoras betwixt Parmenides and Plato », ''Philosophy Compass'', vol. 5, 2010, p. 432-442, qui estime qu'aucun argument probant n'a été opposé à la lecture de Platon.</ref>. Curd, pour sa part, nie que le Noûs soit un principe téléologique extérieur fixant un plan au cosmos, tout en laissant ouverte la possibilité d'un ordre immanent aux processus qu'il déclenche<ref>Curd, 2007, p. 144 et 204-205.</ref>. Kurt von Fritz voyait dans le Noûs la jonction mal accordée de deux héritages, l'intelligence cognitive venue de Parménide et le principe moteur des Milésiens ; André Laks résume cette lecture par la formule paradoxale d'une « intelligence aveugle » et assigne au Noûs un but premier, séparer du mélange des entités distinctes, interprétation que discute Joseph DeFilippo<ref>Kurt von Fritz, « Der ΝΟΥΣ des Anaxagoras », ''Archiv für Begriffsgeschichte'', vol. 9, 1964, p. 87-102, repris dans ''Grundprobleme der Geschichte der antiken Wissenschaft'', Berlin et New York, De Gruyter, 1971, p. 576-593 ; Joseph G. DeFilippo, « Reply to André Laks on Anaxagoras' ΝΟΥΣ », ''Southern Journal of Philosophy'', vol. 31, suppl., 1993, p. 39-48, ici p. 39-40 et 46 ; Curd et Sisko, ''SEP'', 2026, § 4.2.</ref>.
=== La critique platonicienne et aristotélicienne ===
Dans le ''Phédon'', Platon fait raconter par Socrate sa déception à la lecture du livre d'Anaxagore :
<blockquote>Un jour, j'entendis quelqu'un lire dans un livre d'Anaxagore, disait-il, que c'est l'Intellect qui met tout en ordre et qui est la cause de toutes choses. Cette cause me réjouit, et il me sembla qu'il était bon, d'une certaine manière, que l'Intellect fût la cause de tout ; et je pensai que, s'il en est ainsi, l'Intellect, en ordonnant, ordonne tout et dispose chaque chose de la manière qui est la meilleure. [...] Mais cette merveilleuse espérance, mon ami, me fut ôtée lorsque, poursuivant ma lecture, je vis un homme qui ne faisait aucun usage de l'Intellect, qui ne lui attribuait aucune responsabilité dans l'ordonnance des choses, mais qui alléguait comme causes des airs, des éthers, des eaux et bien d'autres choses étranges.<ref>Platon, ''Phédon'', 97b-98c (DK 59 A 47).</ref></blockquote>
La portée de ce passage doit être saisie exactement. Socrate ne dit pas qu'Anaxagore avait proposé une explication par le meilleur et l'avait mal développée : il dit qu'il ''espérait'' en trouver une, et qu'il fut déçu. La formule « disposer chaque chose de la manière qui est la meilleure » exprime l'attente socratique, non la doctrine d'Anaxagore.
Le texte d'Anaxagore, dans les fragments que nous possédons, affirme que le Noûs connaît toutes choses et qu'il les ordonne par l'intermédiaire de la révolution cosmique. Il n'affirme pas que cet ordre soit le meilleur possible. C'est Socrate qui, lisant Anaxagore, infère que, si le monde est ordonné par un Intellect, il doit l'être en vue du bien. Curd juge que Platon a raison de constater l'absence, chez Anaxagore, d'un bien indépendant qui servirait de principe d'explication ; elle note que cette exigence, chez Platon, conduira à la Forme du Bien, puis au Démiurge du ''Timée''<ref>Curd, 2007, p. 144-145 et 204.</ref>. Il faut donc distinguer l'attribution au Noûs d'un rôle moteur et cognitif, qui est anaxagoréenne, et celle d'un principe évaluatif selon lequel le monde serait disposé en vue du meilleur, qui ne se trouve pas dans les fragments. Le reproche socratique ne dénonce pas une incohérence interne ; il regrette qu'Anaxagore ne soit pas allé jusqu'à l'explication que Socrate attendait.
Aristote reprend, dans la ''Métaphysique'', le reproche d'un usage insuffisant du Noûs :
<blockquote>Anaxagore se sert de l'Intellect comme d'un ''deus ex machina'' pour la fabrication du monde ; et quand il est embarrassé pour dire par quelle cause une chose est nécessairement, il le fait intervenir ; mais dans les autres cas, il donne pour causes de ce qui arrive toutes choses plutôt que l'Intellect.<ref>Aristote, ''Métaphysique'', A, 4, 985a18-21 (DK 59 A 47).</ref></blockquote>
Aristote précise ailleurs que ceux qui posent l'Intellect ou l'Amitié comme causes les traitent comme des principes du mouvement plutôt que comme des fins<ref>Aristote, ''Métaphysique'', A, 7, 988b6-11.</ref>. Ses autres objections, comme le souligne Curd, ne portent pas d'abord sur l'absence de téléologie : elles visent la confusion entre l'âme et l'intellect, et l'absence d'explication de la manière dont le Noûs connaît<ref>Aristote, ''De l'âme'', I, 2, 404a25-b6 et 405a13-19 (DK 59 A 55, A 99, A 100) ; III, 4, 429a18-24 et 429b22-24 (DK 59 A 100) ; Curd, 2007, p. 146 et 205.</ref>. L'explication par le meilleur, au sens où Socrate l'attendait, ne se trouve donc pas dans les fragments d'Anaxagore ; elle se constitue dans la critique platonicienne, puis dans la doctrine aristotélicienne de la cause finale. Certains interprètes modernes n'en reconnaissent pas moins, chez Anaxagore, une première forme de téléologie cosmique.
=== Synthèse : l'innovation du Noûs ===
L'introduction du Noûs constitue une innovation à plusieurs titres.
Anaxagore est, parmi les penseurs grecs dont nous connaissons les doctrines, le premier à poser explicitement une entité qui, bien qu'elle agisse sur les ingrédients, n'est mêlée à aucun d'eux<ref>Guthrie, 1965, p. 276-279 ; Curd, 2007, p. 193-194 et 200-201. Aristote (''Métaphysique'', A, 3, 984b15-20 = DK 59 A 58) signale que la doctrine était attribuée avant lui à Hermotime de Clazomènes, figure largement légendaire (Curd, 2007, p. 205, n. 23).</ref>. Il s'inscrit néanmoins dans une tradition : l'idée d'une intelligence cosmique qui comprend et gouverne l'univers se trouve déjà chez Xénophane et Héraclite<ref>Curd, 2007, p. 195 et n. 6-7.</ref>. Sa conception offre un précédent auquel on a comparé, par la suite, la doctrine platonicienne de l'âme séparée et celle de l'intellect séparé chez Aristote<ref>Platon, ''Phédon'', 78b-84b ; Aristote, ''De l'âme'', III, 4-5.</ref>.
Anaxagore identifie par ailleurs une cause unique du mouvement cosmique et de l'ordre qui en résulte. Aristote salue cette innovation en disant qu'Anaxagore parut « comme un homme sobre » au milieu de prédécesseurs qui parlaient au hasard<ref>Aristote, ''Métaphysique'', A, 3, 984b15-20 (DK 59 A 58).</ref>. En nommant ce principe Νοῦς et en lui attribuant connaissance, discernement et puissance, Anaxagore associe explicitement l'ordre cosmique à un principe intelligent. Socrate, Platon et Aristote reprendront ce rapprochement en le transformant profondément, au-delà de ce qu'affirment les fragments<ref>Schofield, 1980, p. 59-61, qui juge le Noûs plus proche du Démiurge platonicien que du Dieu créateur de la tradition judéo-chrétienne ; Curd, 2007, p. 143-146.</ref>.
== La cosmogonie et la cosmologie ==
La cosmogonie et la cosmologie d'Anaxagore appliquent ses principes métaphysiques. Anaxagore s'y montre héritier de la tradition ionienne et novateur, proposant des explications naturalistes des phénomènes célestes et météorologiques<ref>Curd, 2007, p. 206-234 ; Gershenson et Greenberg, 1964, p. 34-54 ; Daniel W. Graham, ''Science before Socrates: Parmenides, Anaxagoras, and the New Astronomy'', New York, Oxford University Press, 2013.</ref>. Les témoignages sont ici notre source principale, et ils ne concordent pas toujours.
=== Le mouvement rotatoire cosmogonique ===
Le processus cosmogonique commence lorsque le Noûs imprime au mélange originel un mouvement de révolution (περιχώρησις)<ref>Anaxagore, B12 et B13 (DK 59 B 12-13).</ref>. Ce mouvement prend la forme d'un tourbillon (δίνη) dont l'étendue ne cesse de croître<ref>Aristote, ''Du ciel'', II, 13, 295a9-14 (DK 59 A 88) ; Curd, 2007, p. 207-208.</ref>. Anaxagore affirme que sa rapidité est sans commune mesure avec celle d'aucune chose connue des hommes : elle est « de beaucoup de fois plus rapide »<ref>Anaxagore, fragment B9 (DK 59 B 9).</ref>.
La séparation qui en résulte est d'ordre physique : le tourbillon rassemble au centre les ingrédients denses et lourds et repousse vers la périphérie les ingrédients rares et légers ; selon Aristote, les partisans du tourbillon tiraient cet argument de ce qu'on observe dans l'eau et dans l'air<ref>Aristote, ''Du ciel'', II, 13, 295a9-14 (DK 59 A 88), qui rapporte que, dans les tourbillons des liquides et de l'air, les corps plus grands et plus lourds se portent vers le centre ; Hippolyte, ''Réfutation'', I, 8, 2 (DK 59 A 42).</ref>. Le fragment B15 décrit ce processus :
<blockquote>Le dense, l'humide, le froid et l'obscur se rassemblèrent ici, là où se trouve maintenant la terre ; le rare, le chaud et le sec se retirèrent vers les régions lointaines de l'éther.<ref>Anaxagore, fragment B15, cité par Simplicius, ''Commentaire sur la Physique'', 179, 3-6 (DK 59 B 15). Hippolyte ajoute « le brillant » à la seconde série.</ref></blockquote>
Cette séparation n'est jamais achevée : conformément au principe selon lequel rien n'est complètement séparé (B8, B12), la rotation continue de produire des séparations et des mélanges partiels<ref>Anaxagore, B8 (« les choses qui sont dans l'unique cosmos ne sont pas séparées les unes des autres ni tranchées à la hache ») et B12.</ref>. L'ordre exact des premières séparations, celle de l'air et de l'éther puis celle des opposés et de la terre, reste discuté<ref>D. Bargrave-Weaver, « The Cosmogony of Anaxagoras », ''Phronesis'', vol. 4, 1959, p. 77-91 ; M. C. Stokes, « On Anaxagoras, Part II: The Order of Cosmogony », ''Archiv für Geschichte der Philosophie'', vol. 47, 1965, p. 217-250 ; Ronald Potts, « Anaxagoras' Cosmogony », ''Apeiron'', vol. 18, 1984, p. 90-96.</ref>.
=== La formation de la terre ===
Au centre du tourbillon, les ingrédients denses, humides, froids et obscurs se sont concentrés pour former la terre<ref>Anaxagore, B15 et B16 ; Hippolyte, ''Réfutation'', I, 8, 2 (DK 59 A 42).</ref>. Selon Anaxagore, la terre est plate<ref>Hippolyte, ''Réfutation'', I, 8, 3 (DK 59 A 42) ; Aristote, ''Du ciel'', II, 13, 294b13-21 (DK 59 A 87).</ref>, conception traditionnelle dans la cosmologie ionienne.
Elle demeure immobile parce qu'elle repose sur l'air qui la supporte. Selon Aristote, Anaximène, Anaxagore et Démocrite expliquaient cette stabilité par la platitude de la terre : elle ne fend pas l'air situé au-dessous, mais le recouvre comme un couvercle<ref>Aristote, ''Du ciel'', II, 13, 294b13-21 (DK 59 A 87). Hippolyte (I, 8, 3 = DK 59 A 42) invoque aussi la grandeur de la terre et l'absence de vide.</ref>. Aristote rapporte qu'Anaxagore montrait la résistance de l'air au moyen d'outres gonflées et de clepsydres<ref>Aristote, ''Physique'', IV, 6, 213a22-27 (DK 59 A 68) ; pseudo-Aristote, ''Problèmes'', XVI, 8, 914b9-915a24 (DK 59 A 69) ; Gershenson et Greenberg, 1964, p. 40-43. La clepsydre désigne ici un ustensile servant à transvaser les liquides (Curd, 2007, p. 108, n. 28).</ref>.
=== La formation des corps célestes ===
Les corps célestes se seraient formés à partir de pierres arrachées à la terre par la violence de la rotation, puis enflammées par l'éther brûlant qui les emporte<ref>Aétius, II, 13, 3 (DK 59 A 71) ; Hippolyte, ''Réfutation'', I, 8, 6 (DK 59 A 42) ; Curd, 2007, p. 209.</ref>. Les astres ne sont donc pas des êtres divins, mais des pierres incandescentes<ref>Platon, ''Apologie'', 26d (DK 59 A 35) ; Hippolyte, ''Réfutation'', I, 8, 6 (DK 59 A 42). Cette thèse est au cœur des accusations d'impiété.</ref>.
Anaxagore soutenait que le soleil est une masse de métal incandescent ou une pierre de feu, plus grande que le Péloponnèse, et même plusieurs fois plus grande selon Aétius ; Plutarque lui attribue aussi l'idée que la lune a la taille du Péloponnèse<ref>Diogène Laërce, II, 8 (DK 59 A 1) ; Hippolyte, ''Réfutation'', I, 8, 8 (DK 59 A 42) ; Aétius, II, 20, 6 et II, 21, 3 (DK 59 A 72) ; Plutarque, ''Sur la face qui paraît dans la lune'', 932a. Ces textes sont réunis par Daniel W. Graham et Eric Hintz, « Anaxagoras and the Solar Eclipse of 478 BC », ''Apeiron'', vol. 40, 2007, p. 319-344, ici p. 319.</ref>. L'estimation paraissait extravagante à une époque où l'on voyait dans le soleil un disque de dimensions modestes ; elle reste très inférieure à la taille réelle de l'astre. On a cherché l'origine de ces estimations dans l'observation d'une éclipse de soleil : David Sider, à la suite d'Erich Frank, pensait à celle du 30 avril 463 ; Daniel Graham et Eric Hintz ont proposé l'éclipse annulaire du 17 février 478, dont l'ombre couvrit une grande partie du Péloponnèse. Ce sont des hypothèses<ref>David Sider, « Anaxagoras on the Size of the Sun », ''Classical Philology'', vol. 68, 1973, p. 128-129 ; Graham et Hintz, 2007, p. 321-330 (éclipse de 478 : p. 324) ; Graham, ''Science before Socrates'', 2013 ; Curd et Sisko, ''SEP'', 2026, § 1.</ref>.
La lune, selon Anaxagore, est faite de terre, avec des plaines et des ravins<ref>Hippolyte, ''Réfutation'', I, 8, 10 (DK 59 A 42) ; Diogène Laërce, II, 8 (DK 59 A 1) ; Aétius, II, 30, 2 (DK 59 A 77), qui parle de hauteurs, de plaines et de creux.</ref>. Elle tient sa lumière du soleil<ref>Anaxagore, B18, cité par Plutarque, ''Sur la face qui paraît dans la lune'', 16, 929b (DK 59 B 18) ; Platon, ''Cratyle'', 409a-b (DK 59 A 76).</ref>. Aétius attribue toutefois cette découverte à plusieurs penseurs, depuis Thalès, Pythagore et Parménide ; Daniel Graham en crédite Parménide, et Dirk Couprie a contesté que l'expression « tenir sa lumière du soleil » désigne chez Anaxagore une simple réflexion<ref>Aétius, II, 28, 5 (DK 59 A 77) ; Parménide, DK 28 B 14-15 ; Daniel W. Graham, « La lumière de la lune dans la pensée grecque archaïque », dans André Laks et Claire Louguet (éd.), ''Qu'est-ce que la philosophie présocratique ?'', Lille, Presses universitaires du Septentrion, 2002, p. 351-380, et ''Science before Socrates'', 2013, chap. 3 (voir le compte rendu de Gerard Naddaf, ''Mind'', vol. 125, 2016, p. 945-952) ; Denis O'Brien, « Derived Light and Eclipses in the Fifth Century », ''Journal of Hellenic Studies'', vol. 88, 1968, p. 114-127 ; Dirk L. Couprie, « Anaxagoras on the Light and Phases of the Moon », ''Hyperboreus'', vol. 24, 2018, p. 12-39 ; Curd, 2007, p. 211 et n. 15.</ref>.
Hippolyte crédite Anaxagore d'avoir le premier expliqué les éclipses et les phases de la lune. Une éclipse de lune se produit lorsque la terre s'interpose entre le soleil et la lune ; une éclipse de soleil, lors de la nouvelle lune, lorsque la lune s'interpose entre le soleil et la terre<ref>Hippolyte, ''Réfutation'', I, 8, 9-10 (DK 59 A 42) ; Aétius, II, 29, 6-7 (DK 59 A 77) ; Plutarque, ''Vie de Nicias'', 23 (DK 59 A 18), qui lui attribue le premier exposé écrit de la théorie des phases.</ref>. Cette explication s'accompagne d'un élément étranger à l'astronomie moderne : certaines éclipses de lune seraient dues à des corps invisibles situés au-dessous de la lune. La plupart des historiens y voient une avancée importante de l'astronomie grecque, que Graham et Hintz jugent pour l'essentiel correcte ; Couprie soutient au contraire que l'ombre de la terre servait chez Anaxagore à expliquer la Voie lactée, et que les corps invisibles étaient sa seule explication des éclipses de lune<ref>Hippolyte, ''Réfutation'', I, 8, 6 et 9 (DK 59 A 42) ; Aétius, II, 29, 7 (DK 59 A 77) ; Curd, 2007, p. 211 ; Graham et Hintz, 2007, p. 320, qui voient aussi en lui « peut-être le premier astronome empirique » (p. 341) ; Dirk L. Couprie, « Anaxagoras on the Milky Way and Lunar Eclipses », ''Hyperboreus'', vol. 23, 2017, p. 181-207.</ref>.
La tradition prête aussi à Anaxagore des prédictions, dont celle d'une éclipse de soleil<ref>Philostrate, ''Vie d'Apollonios de Tyane'', I, 2 (DK 59 A 6) ; cf. Hippolyte, ''Réfutation'', I, 8, 13 (DK 59 A 42), qui le dit « devin ».</ref>. Les historiens modernes restent sceptiques à l'égard de ces récits<ref>Curd, 2007, p. 132, n. 9-10.</ref>.
La Voie lactée s'explique, selon Anaxagore, par l'ombre que projette la terre lorsque le soleil passe au-dessous d'elle : les étoiles situées dans cette ombre, que le soleil n'éclaire pas, laissent voir leur propre lumière<ref>Aristote, ''Météorologiques'', I, 8, 345a25-31 (DK 59 A 80) ; Aétius, III, 1, 5 ; Hippolyte, ''Réfutation'', I, 8, 10 (DK 59 A 42) ; Curd, 2007, p. 210 ; Couprie, 2017.</ref>.
=== La météorite d'Aigos Potamos ===
La chute d'une grande météorite à Aigos Potamos, sur la rive européenne de l'Hellespont, vers 467 av. J.-C., contribua à la réputation d'Anaxagore<ref>Marbre de Paros, ép. 57, et Pline l'Ancien, ''Histoire naturelle'', II, 149 (DK 59 A 11) ; Plutarque, ''Vie de Lysandre'', 12 (DK 59 A 12) ; Diogène Laërce, II, 10 (DK 59 A 1). Le Marbre de Paros donne 468/7, Pline la deuxième année de la 78{{e}} Olympiade (467/6).</ref>. Plusieurs sources affirment qu'il l'avait prédite. Burnet jugeait cette prédiction absurde, et Curd rappelle que la chute d'une météorite particulière, en un lieu et à un moment donnés, ne pouvait être prévue<ref>Burnet, 1930, chap. VI ; Curd, 2007, p. 132 et n. 10.</ref>. Selon Plutarque, Anaxagore enseignait que les astres, pierres lourdes maintenues par la tension de la révolution, pouvaient tomber si ce mouvement venait à se relâcher ; Daniel Graham comprend en ce sens que la « prédiction » portait sur la chute de pierres célestes en général, et non sur un événement particulier<ref>Plutarque, ''Vie de Lysandre'', 12 (DK 59 A 12) ; Daniel W. Graham, « Anaxagoras and the Comet », ''Ancient Philosophy'', vol. 33, 2013, p. 1-18 ; Evangelos Th. Theodossiou, P. G. Niarchos, V. N. Manimanis et Wayne Orchiston, « The Fall of a Meteorite at Aegos Potami in 467/466 BC », ''Journal of Astronomical History and Heritage'', vol. 5, 2002, p. 135-140.</ref>. Plutarque rapporte aussi, d'après Daïmachos, qu'un corps enflammé fut observé pendant soixante-quinze jours avant la chute<ref>Plutarque, ''Vie de Lysandre'', 12 (DK 59 A 12) ; Curd, 2007, p. 132.</ref>. Pline dit même qu'Anaxagore avait annoncé la chute d'une pierre venue du soleil ; P. J. Bicknell a suggéré que ce récit pourrait garder le souvenir de l'observation d'une tache solaire<ref>Pline l'Ancien, ''Histoire naturelle'', II, 149 (DK 59 A 11) ; P. J. Bicknell, « Did Anaxagoras Observe a Sunspot in 467 B.C.? », ''Isis'', vol. 59, 1968, p. 87-90. L'astronome N. J. Woolf voit dans cette chute l'origine du modèle d'Anaxagore pour les phases et les éclipses de la lune, ce qui reste une hypothèse (« Anaxagoras and the Scientist/Laity Interaction », ''Vistas in Astronomy'', vol. 39, 1995, p. 699-709).</ref>. Quoi qu'il en soit, l'événement fut associé au nom d'Anaxagore et parut confirmer sa thèse selon laquelle les corps célestes sont faits de pierre.
=== Météorologie ===
Anaxagore expliquait les nuages et la neige à peu près comme Anaximène<ref>Aétius, III, 4, 2 (DK 59 A 85) ; Curd, 2007, p. 223.</ref>. Le fragment B16 décrit une série de solidifications : des nuages se sépare l'eau, de l'eau la terre, et de la terre les pierres, que le froid solidifie<ref>Anaxagore, fragment B16 (DK 59 B 16).</ref>.
La formation de la grêle posait un problème particulier : comment de la glace peut-elle se former en été ? Selon Aristote, qui critique cette théorie, Anaxagore soutenait que la grêle se forme lorsqu'un nuage est poussé vers la région supérieure, plus froide ; les chaleurs de l'été, en poussant les nuages plus haut, expliqueraient la fréquence des orages de grêle en été et dans les pays chauds<ref>Aristote, ''Météorologiques'', I, 12, 348a14-20 et 348b12-16 (DK 59 A 85), Aétius, III, 4, 2 (DK 59 A 85), qui ajoute que les gouttes s'arrondissent en tombant ; Curd, 2007, p. 223.</ref>.
L'arc-en-ciel, selon le fragment B19, est le reflet du soleil dans les nuages ; il annonce la tempête, car l'eau répandue autour du nuage produit du vent ou se déverse en pluie<ref>Anaxagore, B19, conservé par les scholies BT à l’''Iliade'', XVII, 547 (DK 59 B 19) ; sur le texte, Friedrich Solmsen, « Anaxagoras B 19 Diels-Kranz », ''Hermes'', vol. 91, 1963, p. 251-252.</ref>.
Le tonnerre et l'éclair sont produits, selon Anaxagore, lorsque du feu venu de l'éther tombe dans les nuages : l'éclat du feu est l'éclair, et le sifflement du feu qui s'éteint est le tonnerre<ref>Aristote, ''Météorologiques'', II, 9, 369b14-19 (DK 59 A 84) ; Aétius, III, 3, 4 (DK 59 A 84) ; Sénèque, ''Questions naturelles'', II, 12, 3 et II, 19 (DK 59 A 84).</ref>. Aristote précise qu'Anaxagore tenait l'éclair pour réellement antérieur au tonnerre, les choses se passant comme elles apparaissent ; l'explication de ce décalage par la plus grande vitesse de la vue est celle d'Aristote, non celle d'Anaxagore<ref>Aristote, ''Météorologiques'', II, 9, 369b7-19.</ref>.
Les tremblements de terre sont attribués, selon Aristote, à l'éther qui, tendant naturellement vers le haut, se trouve emprisonné dans les cavités souterraines ; Aétius et Hippolyte parlent plutôt d'air qui pénètre sous la terre et l'ébranle<ref>Aristote, ''Météorologiques'', II, 7, 365a19-35 (DK 59 A 89) ; Aétius, III, 15, 4 (DK 59 A 89) ; Hippolyte, ''Réfutation'', I, 8, 12 (DK 59 A 42).</ref>.
=== Hydrologie ===
Anaxagore expliquait la crue estivale du Nil par la fonte des neiges dans les régions montagneuses d'Éthiopie<ref>Hippolyte, ''Réfutation'', I, 8, 5 (DK 59 A 42) ; Aétius, IV, 1, 3 (DK 59 A 91) ; Sénèque, ''Questions naturelles'', IV a, 2, 17 (DK 59 A 91) ; Schofield (1980, p. 34) relève qu'Eschyle adopte cette explication dans les ''Suppliantes'' (559-561).</ref>. Hérodote jugeait déjà cette explication la plus séduisante et la plus fausse, et Sénèque la rejette. Elle localise à juste titre dans les régions éthiopiennes une source majeure de la crue estivale, mais l'attribue à tort à la fonte des neiges plutôt qu'aux pluies saisonnières<ref>Hérodote, II, 22 (DK 59 A 91) ; Curd, 2007, p. 225.</ref>. Quant à la mer, elle serait née de l'eau stagnante originelle, dont la partie la plus fine s'évapora sous l'effet du soleil, laissant un résidu salé et amer<ref>Aétius, III, 16, 2 (DK 59 A 90) ; Hippolyte, ''Réfutation'', I, 8, 4 (DK 59 A 42) ; Diogène Laërce, II, 8 (DK 59 A 1).</ref>.
=== D'autres mondes ? ===
Le fragment B4a affirme que des hommes et d'autres animaux se sont composés, qu'il existe des cités habitées et des champs cultivés « comme chez nous », un soleil, une lune et d'autres astres « comme chez nous » ; Anaxagore précise qu'il a dit cela de la séparation parce qu'elle se produirait non seulement chez nous, mais aussi ailleurs<ref>Anaxagore, B4a (DK 59 B 4a), cité par Simplicius, ''Commentaire sur la Physique'', 34, 29-35, 9, et commenté en 157, 9-24.</ref>. Le sens de cet « ailleurs » est discuté. Hermann Fränkel y voyait une expérience de pensée. Jaap Mansfeld objecte que le texte ne présente pas cet autre monde comme hypothétique ; s'appuyant sur B3 et B6, il propose d'y voir un monde infiniment petit, situé au-dessous du seuil de la perception et en tout semblable au nôtre, parce qu'il est fait des mêmes composants<ref>Jaap Mansfeld, « Anaxagoras' Other World », ''Phronesis'', vol. 25, 1980, p. 1-4, ici p. 2-3, qui discute aussi la lecture de Fränkel, reprend une idée de P. Leon (1927) et compare ces mondes emboîtés à l'image répétée à l'infini d'une boîte de chocolats Droste. Aétius (II, 1, 2 = DK 59 A 63) range d'ailleurs Anaxagore parmi les partisans d'un monde unique.</ref>. D'autres interprètes y voient des mondes séparés dans l'espace, ou des tourbillons secondaires en bordure du tourbillon principal ; John Sisko propose des mondes emboîtés vers le petit comme vers le grand<ref>Curd, 2007, p. 213 ; Curd et Sisko, ''SEP'', 2026, § 4.3 ; Sisko, 2003, p. 87-114.</ref>.
=== Synthèse : la cosmologie naturaliste d'Anaxagore ===
La cosmologie d'Anaxagore se caractérise par trois traits.
Le premier est l'unité de la nature : les corps célestes sont faits des mêmes ingrédients que la terre, puisque ce sont des pierres arrachées à celle-ci<ref>Aétius, II, 13, 3 (DK 59 A 71) ; Hippolyte, ''Réfutation'', I, 8, 6 (DK 59 A 42) ; Gershenson et Greenberg, 1964, p. 47.</ref>. Il n'existe pas de différence de nature entre le monde sublunaire et le ciel, contrairement à ce qu'affirmera Aristote<ref>Aristote, ''Du ciel'', I, 2-3, 268b11-270b25.</ref>.
Le deuxième est l'explication physique : une fois la rotation initiée par le Noûs, les phénomènes cosmologiques et météorologiques s'expliquent par des processus tels que la rotation, la séparation selon la densité, l'évaporation ou la condensation, sans recours à des interventions divines<ref>Curd, 2007, p. 222 ; Gershenson et Greenberg, 1964, p. 26.</ref>.
Le troisième est l'usage de l'analogie : Anaxagore explique les phénomènes célestes par analogie avec des phénomènes terrestres observables<ref>Gershenson et Greenberg, 1964, p. 24 et 34.</ref>.
Ces traits donnent à la cosmologie d'Anaxagore un caractère naturaliste marqué, qui représente une étape importante dans l'histoire de l'explication physique de la nature. Il ne faut pas pour autant voir en lui le « fondateur de la méthode scientifique », selon une expression que l'on rencontre parfois ; mieux vaut parler d'un jalon, où se combinent intuitions fécondes et erreurs caractéristiques de son temps<ref>La formule forte est celle de Gershenson et Greenberg (1964, préface), qui font d'Anaxagore le premier savant au sens moderne. Pour une perspective plus prudente, voir G. E. R. Lloyd, ''Early Greek Science: Thales to Aristotle'', Londres, Chatto & Windus, 1970.</ref>.
== La physiologie et la biologie ==
Bien qu'Anaxagore soit surtout connu pour sa cosmologie, les témoignages indiquent qu'il s'intéressa aussi à la nutrition, à la perception, à l'embryologie et aux êtres vivants<ref>Curd, 2007, p. 225-229 ; Gershenson et Greenberg, 1964, p. 55-57 ; Hubert Erhard, « Anaxagoras als Biologe », ''Sudhoffs Archiv für Geschichte der Medizin und der Naturwissenschaften'', vol. 35, 1942, p. 117-140.</ref>.
=== La théorie de la nutrition ===
L'une des questions biologiques qu'Anaxagore cherche à élucider est celle de la nutrition : comment une nourriture simple, comme le pain et l'eau, peut-elle nourrir les cheveux, les veines, les artères, la chair, les nerfs et les os ?<ref>Aétius, I, 3, 5 (DK 59 A 46) ; Simplicius, ''Commentaire sur la Physique'', 460, 4-20 (DK 59 A 45) ; Platon, ''Phédon'', 96c-d (DK 59 A 46) ; Lucrèce, ''De la nature'', I, 830-920 (DK 59 A 44).</ref> Cette observation pose un problème au regard de l'interdit parménidien : une substance nouvelle, la chair, semble naître de ce qui n'est pas chair.
La réponse d'Anaxagore est cohérente avec sa métaphysique : le pain doit déjà contenir de la chair, du sang, des os et toutes les autres substances corporelles, en proportions si faibles qu'elles restent imperceptibles<ref>Simplicius, ''Commentaire sur la Physique'', 460, 4-20 (DK 59 A 45), qui ajoute que, si les arbres se nourrissent d'eau, l'eau doit contenir du bois, de l'écorce et des fruits.</ref>. Lorsque nous mangeons, la chair contenue dans la nourriture vient s'ajouter à la chair du corps<ref>Aétius, I, 3, 5 (DK 59 A 46) ; Simplicius, ''Commentaire sur la Physique'', 460, 4-20 (DK 59 A 45).</ref>.
Cette théorie soulève une autre difficulté : comment le corps dirige-t-il la chair vers les muscles et les parties osseuses vers les os ? Les fragments ne le disent pas. Certains interprètes supposent qu'Anaxagore rattachait cette fonction au Noûs présent dans les êtres vivants<ref>Gershenson et Greenberg, 1964, p. 24, pour qui la digestion est, chez Anaxagore, une fonction de l'intellect ; Curd (2007, p. 175-177) suggère que les semences contiennent, par le Noûs ou l'âme, un principe d'organisation, tout en qualifiant cette reconstruction de spéculative.</ref>.
=== La théorie de la perception ===
Anaxagore élabora une théorie de la perception fondée sur le principe que le semblable n'est pas affecté par le semblable : la perception se fait par les contraires<ref>Théophraste, ''Du sens'', 27 (DK 59 A 92).</ref>. Il s'opposait ainsi à Empédocle, pour qui la perception se fait par le semblable<ref>Théophraste, ''Du sens'', 1-2 (DK 31 A 86).</ref>.
Pour qu'une perception ait lieu, il doit exister une différence entre l'organe et l'objet perçu : ce qui est exactement aussi chaud ou aussi froid que nous ne nous réchauffe ni ne nous refroidit à son contact<ref>Théophraste, ''Du sens'', 28 (DK 59 A 92).</ref>. La vision se fait par le reflet de l'objet dans la pupille, qui ne se produit que sur une couleur différente<ref>Théophraste, ''Du sens'', 27 (DK 59 A 92) ; Gershenson et Greenberg, 1964, p. 28-32.</ref>.
Anaxagore en concluait que toute perception s'accompagne de douleur ou de gêne (λύπη), car tout contact du dissemblable produit une irritation, sensible lorsque la sensation est trop longue ou trop intense<ref>Théophraste, ''Du sens'', 29 (DK 59 A 92) ; Aétius, IV, 9, 16, et Aristote, ''Éthique à Nicomaque'', VII, 15, 1154b7-9 (DK 59 A 94) ; James Warren, « Anaxagoras on Perception, Pleasure, and Pain », ''Oxford Studies in Ancient Philosophy'', vol. 33, 2007, p. 19-54 ; Curd, 2007, p. 169 et n. 37.</ref>.
=== La reproduction et l'embryologie ===
Les témoignages sur l'embryologie d'Anaxagore ne concordent pas entièrement. Selon Aristote, il comptait parmi ceux pour qui la différence des sexes est déjà présente dans la semence : la semence vient du mâle, la femelle ne fournissant que le lieu, et les mâles proviennent du côté droit, les femelles du côté gauche<ref>Aristote, ''Génération des animaux'', IV, 1, 763b30-33 (DK 59 A 107) ; Hippolyte, ''Réfutation'', I, 8, 12 (DK 59 A 42). Schofield (1980, p. 34) relève que l'idée selon laquelle la mère ne fournit qu'un lieu apparaît dans les ''Euménides'' d'Eschyle (657-666).</ref>. D'autres sources lui prêtent l'idée d'une contribution séminale des deux parents, l'enfant ressemblant à celui qui a fourni le plus de semence<ref>Censorinus, ''Du jour natal'', 5, 2-4 et 6, 6-8 (DK 59 A 107 et A 111) ; Aétius, V, 7, 4 (DK 59 A 111), qui associe Anaxagore et Parménide ; Curd, 2007, p. 174 et 226-227.</ref>. Owen Kember juge le témoignage d'Aristote inexact sur plusieurs points et reconstruit ainsi la théorie : les deux parents émettraient une semence venue de tout le corps, la part de chacun déterminerait la ressemblance, et le sexe dépendrait du côté de l'utérus où se dépose le mélange<ref>Owen Kember, « Anaxagoras' Theory of Sex Differentiation and Heredity », ''Phronesis'', vol. 18, 1973, p. 1-14, ici p. 1 et 14, contre l'interprétation reçue (Blersch, Erhard, Lesky, Lloyd), fondée sur Aristote.</ref>.
=== La génération des animaux et des plantes ===
Selon les témoignages, Anaxagore distinguait la première génération des animaux et leur reproduction ultérieure : les animaux naquirent d'abord de l'humide, du chaud et du terreux, puis les uns des autres<ref>Diogène Laërce, II, 9 (DK 59 A 1) ; Hippolyte, ''Réfutation'', I, 8, 12 (DK 59 A 42).</ref>. Irénée lui attribue l'idée que les animaux sont issus de semences tombées du ciel, et Théophraste celle que l'air contient les semences des plantes, entraînées vers le sol par la pluie<ref>Irénée, ''Contre les hérésies'', II, 14, 2 (DK 59 A 113) ; Théophraste, ''Recherches sur les plantes'', III, 1, 4 (DK 59 A 117) ; Schofield, 1980, p. 125.</ref>.
Anaxagore aurait tenu les plantes pour des animaux attachés au sol<ref>Plutarque, ''Questions naturelles'', 1, 911d (DK 59 A 116).</ref>. Le traité pseudo-aristotélicien ''Sur les plantes'' lui prête l'idée qu'elles éprouvent désir, plaisir et peine, en invoquant la chute de leurs feuilles, et qu'elles possèdent intellect et connaissance<ref>Pseudo-Aristote, ''Sur les plantes'', I, 1, 815a15-20 et 815b16-17 (DK 59 A 117). Schofield (1980, p. 148, n. 40-41) juge difficile d'accorder crédit aux sentiments prêtés aux plantes, mais admet que l'attribution d'une âme et d'un intellect puisse reposer sur une source fiable.</ref>.
=== Observations biologiques diverses ===
Les sources conservent quelques observations isolées. Aristote rapporte qu'Anaxagore et d'autres physiciens prétendaient que le corbeau et l'ibis s'accouplent par le bec et que la belette met bas par la gueule, et il leur reproche d'en parler trop superficiellement et sans examen<ref>Aristote, ''Génération des animaux'', III, 6, 756b13-17 (DK 59 A 114).</ref>. L'erreur sur la belette pourrait venir de l'observation de femelles transportant leurs petits dans la gueule aussitôt après la mise bas<ref>Gershenson et Greenberg, 1964, p. 57.</ref>. Ces erreurs montrent les limites de l'observation dans la biologie d'Anaxagore. Il expliquait aussi que ce qu'on appelle proverbialement « lait d'oiseau » n'est autre que le blanc de l'œuf<ref>Anaxagore, B22, cité par Athénée, II, 57d (DK 59 B 22) ; sur le texte, Tad Brennan, « The Text of Anaxagoras Fragment DK 59 B22 », ''American Journal of Philology'', vol. 116, 1995, p. 533-537, qui y voit une explication par réduction comparable à celle de l'arc-en-ciel chez Xénophane.</ref>
=== L'homme et les animaux ===
Selon Anaxagore, l'homme est le plus intelligent des animaux parce qu'il a des mains ; Aristote renverse la formule et soutient que l'homme a des mains parce qu'il est le plus intelligent<ref>Aristote, ''Parties des animaux'', IV, 10, 687a7-10 (DK 59 A 102) ; Schofield, 1980, p. 16.</ref>. Burnet en tirait l'idée que, l'intellect étant partout le même, les différences d'intelligence entre les êtres vivants dépendent de la structure de leur corps<ref>Burnet, 1930, chap. VI.</ref>.
== L'influence et la postérité ==
Anaxagore occupe une place importante dans l'histoire de la philosophie antique. Premier philosophe de renom établi à Athènes, il y introduit la tradition ionienne de recherche sur la nature<ref>Curd, 2007, p. 129 et 142-146 ; Curd et Sisko, ''SEP'', 2026, § 6.</ref>. Les lignes qui suivent décrivent son influence avec la prudence nécessaire : il est facile, en traitant des présocratiques, de céder à la tentation des grandes filiations, alors que la documentation invite souvent à plus de retenue.
=== La transmission immédiate : Archélaos, Diogène d'Apollonie et le papyrus de Derveni ===
La tradition fait d'Archélaos d'Athènes un disciple ou un successeur d'Anaxagore et, dans certaines sources, le maître de Socrate ; il est ainsi l'un des premiers relais auxquels la doxographie rattache la pensée anaxagoréenne<ref>Diogène Laërce, II, 16 (DK 60 A 1) ; Curd, 2007, p. 134 et n. 18. Ion de Chios rapporte que le jeune Socrate voyagea à Samos avec Archélaos (Diogène Laërce, II, 23 = DK 60 A 3).</ref>. Clément d'Alexandrie en fait le successeur d'Anaxagore, et Eusèbe précise qu'il lui succéda à la tête de l'école de Lampsaque, indication difficile à concilier avec les témoignages qui situent son enseignement à Athènes<ref>Clément d'Alexandrie, ''Stromates'', I, 63, et Eusèbe, ''Préparation évangélique'', X, 14, 13 (DK 59 A 7) ; Curd, 2007, p. 134, n. 18.</ref>. Archélaos semble avoir prolongé la cosmologie de son maître par un récit des origines des institutions sociales ; Gábor Betegh voit en lui l'un des premiers auteurs à avoir uni ces deux récits, innovation apparue à l'époque de Socrate<ref>Gábor Betegh, « Archelaus on Cosmogony and the Origins of Social Institutions », ''Oxford Studies in Ancient Philosophy'', vol. 51, 2016, p. 1-40.</ref>.
Diogène d'Apollonie, dans la génération suivante, subit l'influence d'Anaxagore dans sa doctrine comme dans son style ; il fait de l'intelligence un principe cosmique, mais l'attribue à l'air et prend soin de l'appuyer sur des arguments, ce qu'Anaxagore ne faisait guère<ref>Schofield, 1980, p. 5-6 ; Curd, « Presocratic Philosophy », ''SEP'', 2020, § 9.</ref>. L'auteur du papyrus de Derveni, commentaire allégorique d'un poème orphique découvert en 1962, semble avoir repris, en les modifiant, la doctrine d'un Intellect qui gouverne le cosmos et certains aspects de la théorie des ingrédients<ref>Curd, 2007, p. 143 et n. 35 ; Gábor Betegh, ''The Derveni Papyrus: Cosmology, Theology and Interpretation'', Cambridge, Cambridge University Press, 2004, chap. 7.</ref>.
=== L'influence sur Socrate : espoirs et déceptions ===
La relation entre Anaxagore et Socrate passe, dans nos sources, par la lecture du livre. Dans le ''Phédon'', Socrate raconte sa rencontre avec la pensée d'Anaxagore, qui suscita d'abord son enthousiasme, puis sa déception<ref>Platon, ''Phédon'', 97b-98c (DK 59 A 47).</ref>. Comme on l'a vu, cette déception porte sur une attente proprement socratique, celle d'une explication par le meilleur, qu'Anaxagore n'avait pas formulée dans les termes que Socrate aurait voulus.
Le ''Phédon'' présente ensuite le passage de Socrate à une autre méthode de recherche des causes, fondée sur les Formes, que Socrate lui-même présente comme une « seconde navigation »<ref>Platon, ''Phédon'', 99c-100a ; Curd, 2007, p. 144-145.</ref>. L'idée qu'un principe intelligent gouverne l'univers, que Socrate trouve chez Anaxagore et juge insuffisamment exploitée, a pu orienter sa recherche sans qu'il faille faire d'Anaxagore sa cause nécessaire.
Dans l’''Apologie'', Mélétos attribue à Socrate des thèses cosmologiques que celui-ci renvoie explicitement aux livres d'Anaxagore ; Platon fait ainsi de la confusion entre Socrate et la philosophie naturelle anaxagoréenne un élément de la défense de Socrate<ref>Platon, ''Apologie de Socrate'', 26d-e (DK 59 A 35) ; Curd, 2007, p. 142-143.</ref>. Xénophon présente de son côté Socrate mettant ses disciples en garde contre l'étude des phénomènes célestes à la manière d'Anaxagore, qu'il accuse d'avoir perdu la raison en identifiant le soleil au feu<ref>Xénophon, ''Mémorables'', IV, 7, 6-7 (DK 59 A 73).</ref>.
=== L'appropriation platonicienne : du Noûs au Démiurge ===
Platon reprend la doctrine du Noûs et la transforme en une cosmologie téléologique. Dans le ''Timée'', le Démiurge, artisan divin qui façonne le monde sensible en contemplant les Formes, peut se lire en dialogue avec le Noûs d'Anaxagore<ref>Platon, ''Timée'', 29a-30c et 47e-48a ; Curd, 2007, p. 145 ; Schofield, 1980, p. 61 ; Glenn R. Morrow, « Necessity and Persuasion in Plato's ''Timaeus'' », ''Philosophical Review'', vol. 59, 1950, p. 147-163 ; Luc Brisson, ''Le Même et l'Autre dans la structure ontologique du Timée de Platon'', Paris, Klincksieck, 1974.</ref>. La téléologie platonicienne va cependant bien au-delà de ce qu'affirmait Anaxagore : le Démiurge façonne le monde en vue du meilleur en contemplant des réalités intelligibles, arrière-plan qui n'a pas d'équivalent dans les fragments. Schofield note toutefois un point commun : pour Anaxagore comme pour Platon, c'est l'ordre du monde qui fait problème, non l'existence des choses, et la matière est présupposée<ref>Schofield, 1980, p. 61.</ref>.
Dans le ''Philèbe'', Socrate rappelle l'accord des sages pour dire que l'Intellect est roi du ciel et de la terre<ref>Platon, ''Philèbe'', 28c ; cf. ''Cratyle'', 413c (DK 59 A 55), où la justice est identifiée à l'Intellect « selon Anaxagore ».</ref>. Dans les ''Lois'', Platon évoque ceux qui ont osé dire que c'est l'Intellect qui a ordonné tout ce qui est dans le ciel, allusion probable à Anaxagore<ref>Platon, ''Lois'', XII, 967b-c.</ref>. Curd montre en outre que Platon réemploie le vocabulaire anaxagoréen : les Formes sont, comme le Noûs, « elles-mêmes par elles-mêmes », et les choses sensibles « participent » des Formes comme, chez Anaxagore, les choses ont part à toutes choses<ref>Curd, 2007, p. 50 et 143-145.</ref>. Platon reprend ainsi le motif anaxagoréen d'un Intellect ordonnateur en le transformant : le Noûs, principe moteur et cognitif, devient chez lui un Intellect qui façonne le monde selon le meilleur ordre possible.
=== La critique aristotélicienne ===
Aristote, tout en reconnaissant l'importance historique d'Anaxagore, en fut l'un des critiques les plus sévères. Dans la ''Métaphysique'', il dit qu'Anaxagore parut comme un homme sobre au milieu de prédécesseurs qui parlaient au hasard<ref>Aristote, ''Métaphysique'', A, 3, 984b15-20 (DK 59 A 58).</ref>, avant de lui reprocher de se servir de l'Intellect comme d'un ''deus ex machina''<ref>Aristote, ''Métaphysique'', A, 4, 985a18-21 (DK 59 A 47).</ref>. Ses critiques portent aussi, dans le traité ''De l'âme'', sur la confusion entre l'âme et l'intellect et sur la manière dont le Noûs connaît<ref>Aristote, ''De l'âme'', I, 2 et III, 4 (DK 59 A 55, A 99, A 100) ; Curd, 2007, p. 146.</ref>.
Aristote décrit par ailleurs la doctrine d'Anaxagore au moyen de son propre vocabulaire, notamment le terme d'homéomère (ὁμοιομερής), qu'Anaxagore n'a probablement jamais employé<ref>Curd, 2007, p. 147-150 ; Schofield, 1980, p. 128-132.</ref>. La distinction entre parties homéomères (chair, os, sang) et anhoméomères (main, visage) appartient à la biologie d'Aristote lui-même ; il s'en sert pour décrire rétrospectivement la doctrine d'Anaxagore<ref>Aristote, ''Parties des animaux'', II, 1, 646a12-24 ; ''Génération et corruption'', I, 1, 314a18-20 (DK 59 A 46).</ref>.
=== L'héritage dans la philosophie hellénistique et tardive ===
Après Aristote, la pensée d'Anaxagore continua d'exercer une influence diffuse. La doctrine stoïcienne d'une raison divine immanente qui gouverne la nature présente des analogies avec le Noûs, mais les Stoïciens, matérialistes, rejettent la séparation de l'intellect et des corps, et aucune dépendance directe n'est établie<ref>A. A. Long et D. N. Sedley, ''The Hellenistic Philosophers'', vol. I, Cambridge, Cambridge University Press, 1987 ; Michael J. White, « Stoic Natural Philosophy (Physics and Cosmology) », dans Brad Inwood (éd.), ''The Cambridge Companion to the Stoics'', Cambridge, Cambridge University Press, 2003, p. 124-152.</ref>.
Du côté épicurien, Lucrèce critique longuement l’''homoeomeria'' d'Anaxagore<ref>Lucrèce, ''De la nature'', I, 830-920 (DK 59 A 44) ; David Sedley, ''Lucretius and the Transformation of Greek Wisdom'', Cambridge, Cambridge University Press, 1998.</ref>, et Cicéron fait railler par l'épicurien Velléius son Intellect illimité<ref>Cicéron, ''De la nature des dieux'', I, 11, 26 (DK 59 A 48).</ref>. Selon Dioclès, cité par Diogène Laërce, Épicure approuvait pourtant Anaxagore plus que tout autre ancien, tout en s'en écartant sur certains points<ref>Diogène Laërce, X, 12 (DK 59 A 26).</ref>.
Dans la tradition néoplatonicienne, Anaxagore est lu à travers Platon et Aristote ; les commentateurs formulent ses principes du « tout en tout » et de la prédominance dans leur propre vocabulaire<ref>Proclus, ''Éléments de théologie'', 103 ; Schofield, 1980, p. 155, n. 52.</ref>. Simplicius, au VI{{e}} siècle, consacre de longs passages de son commentaire sur la ''Physique'' d'Aristote à Anaxagore : c'est à lui que nous devons la plupart des fragments<ref>Simplicius, ''Commentaire sur la Physique'', passim ; Teodorsson, 1982, p. 10.</ref>.
=== Échos littéraires ===
À l'époque moderne, Goethe met en scène Anaxagore face à Thalès dans la « Nuit de Walpurgis classique » du second ''Faust'' (acte II). Leur dispute sur la formation des roches et des montagnes, par le feu ou par l'eau, a longtemps été lue comme une parodie de la querelle du XVIII{{e}} siècle entre vulcanistes et neptunistes ; Kenneth Weisinger juge cette lecture insuffisante<ref>Kenneth D. Weisinger, « A Note on Homunculus, Thales, and Anaxagoras », ''Monatshefte'', vol. 64, 1972, p. 237-246, ici p. 237.</ref>.
=== Un héritage à ne pas surinterpréter ===
Il est tentant de présenter Anaxagore comme un précurseur de la science mécaniste moderne, de l'atomisme ou de la physique contemporaine. Il faut résister à cette tentation. Au XVII{{e}} siècle, certains philosophes mécanistes ont invoqué les présocratiques, mais c'est l'atomisme de Leucippe et de Démocrite, transmis par Épicure et Lucrèce, qui a nourri la physique corpusculaire de Gassendi, de Boyle ou de Newton. L'influence propre d'Anaxagore sur cette tradition reste limitée et indirecte : sa doctrine de la divisibilité illimitée et du mélange universel s'oppose aux postulats de l'atomisme<ref>Alan Chalmers, « Atomism from the 17th to the 20th Century », ''Stanford Encyclopedia of Philosophy'', 2005 (révisions ultérieures).</ref>.
Il faut aussi se garder de faire rétrospectivement du Noûs un Dieu créateur : il ordonne un mélange qu'il ne crée pas et qu'il trouve déjà donné<ref>Schofield, 1980, p. 61 ; Curd, 2007, p. 61 et 201, n. 17.</ref>.
Une dernière mise en garde concerne la prédiction d'éclipses et la chute de la météorite d'Aigos Potamos, parfois présentées comme des triomphes d'une « méthode scientifique » naissante. Les sources qui attribuent à Anaxagore ces prédictions sont tardives et souvent légendaires ; les historiens se montrent prudents, voire sceptiques<ref>Curd, 2007, p. 132 et n. 9-10 ; Burnet, 1930, chap. VI ; Graham, « Anaxagoras and the Comet », 2013.</ref>. L'importance d'Anaxagore ne tient pas à la précision de ses prédictions, mais à la cohérence et à l'ambition explicative de son système naturaliste, qui rend compte des phénomènes célestes par des principes physiques accessibles à la raison.
=== Bilan ===
La réception d'Anaxagore est marquée par un contraste. D'un côté, Aristote salue l'introduction du Noûs comme principe cosmique et loue sa sobriété ; de l'autre, Socrate, Platon et Aristote jugent insuffisant l'usage qu'il en fait. Anaxagore apparaît ainsi à la fois comme le penseur qui a rendu pensable une cause intellectuelle du cosmos et comme celui qui, au goût de ses successeurs, l'a laissée inemployée<ref>Curd, 2007, p. 142-146 et 204-205 ; Schofield, 1980, p. 59-61 ; Graham, ''Science before Socrates'', 2013.</ref>.
Cette limite même a été féconde. Le Socrate du ''Phédon'' se tourne vers la recherche de causes que le Noûs anaxagoréen ne fournissait pas. Platon développe, dans le ''Timée'', une cosmologie téléologique où le Démiurge façonne le monde en contemplant les Formes. Aristote élabore, contre Platon autant que contre Anaxagore, une doctrine des causes qui intègre la cause finale. Chez Socrate tel que le présente Platon, chez Platon lui-même et chez Aristote, la doctrine d'Anaxagore fournit à la fois un point d'appui et un objet de critique, au prix de transformations propres à chacun ; leurs doctrines ont leurs propres fondements.
Sur le plan scientifique, l'héritage d'Anaxagore est surtout celui d'un modèle d'explication naturaliste. Le soleil n'est plus Hélios mais une pierre incandescente ; la lune n'est plus une déesse mais un corps terreux qui reçoit sa lumière du soleil ; la foudre n'est plus le trait de Zeus mais un phénomène atmosphérique. Cet héritage a nourri durablement la tradition philosophique et scientifique grecque. Il serait pourtant excessif d'en faire le fondateur de la méthode scientifique moderne, dont la constitution dépend de développements bien postérieurs, notamment la quantification, la modélisation mathématique, l'expérimentation contrôlée et les procédures collectives de vérification<ref>Gershenson et Greenberg (1964) défendent une version forte de la thèse d'Anaxagore fondateur de la méthode scientifique, à nuancer ; cf. Lloyd, 1970.</ref>.
Anaxagore occupe en définitive une place singulière : celle d'un pionnier qui ouvre une voie sans la parcourir entièrement, et qui lègue à ses successeurs à la fois une doctrine et la tâche de penser ce qu'elle laisse ouvert. Placé à la rencontre de la tradition cosmologique ionienne et de la philosophie classique athénienne, dont il contribue à préparer certains problèmes, il mérite d'être étudié pour lui-même, et non seulement à travers les réactions qu'il a suscitées chez Socrate, Platon et Aristote.
== Notes et références ==
{{references|colonnes=2}}
== Bibliographie ==
=== Textes anciens : éditions et traductions ===
; Diels, Hermann & Kranz, Walther (éd.)
: ''Die Fragmente der Vorsokratiker'', 6{{e}} éd., Berlin, Weidmann, 1951-1952, 3 vol.
: [Anaxagore : vol. II, p. 5-44, chap. 59, numérotation DK]
; Laks, André & Most, Glenn W. (éd.)
: ''Early Greek Philosophy'', Cambridge (Mass.), Harvard University Press, 2016, 9 vol. (Loeb Classical Library)
: [Anaxagore : chap. 25, numérotation LM]
; Laks, André & Most, Glenn W. (éd.)
: ''Les Débuts de la philosophie. Des premiers penseurs grecs à Socrate'', Paris, Fayard, 2016
; Curd, Patricia (éd.)
: ''Anaxagoras of Clazomenae: Fragments and Testimonia. A Text and Translation with Notes and Essays'', Toronto, University of Toronto Press, 2007 (Phoenix Presocratics, 6 ; Phoenix Supplementary Volume, 44)
; Sider, David (éd.)
: ''The Fragments of Anaxagoras, Edited with an Introduction and Commentary'', Meisenheim am Glan, Hain, 1981 (Beiträge zur klassischen Philologie, 118) ; 2{{e}} éd. revue, Sankt Augustin, Academia Verlag, 2005 (International Pre-Platonic Studies)
; Zafiropulo, Jean
: ''Anaxagore de Clazomène'', I : ''Le Mythe grec traditionnel de Thalès à Platon'' ; II : ''Théorie et fragments'', Paris, Les Belles Lettres, 1948 (Collection d'études anciennes)
; Kirk, G. S., Raven, J. E. & Schofield, Malcolm
: ''The Presocratic Philosophers: A Critical History with a Selection of Texts'', 2{{e}} éd., Cambridge, Cambridge University Press, 1983
; Simplicius
: ''In Aristotelis Physicorum libros commentaria'', éd. Hermann Diels, Berlin, Reimer, 1882-1895 (Commentaria in Aristotelem Graeca, 9-10)
; Platon
: ''Phédon'', trad. Monique Dixsaut, Paris, Flammarion, 1991 (GF Flammarion)
; Platon
: ''Apologie de Socrate. Criton'', trad. Luc Brisson, Paris, Flammarion, 1997 (GF Flammarion)
; Platon
: ''Timée. Critias'', trad. Luc Brisson, Paris, Flammarion, 1992 (GF Flammarion)
; Platon
: ''Philèbe'', texte établi et traduit par Auguste Diès, Paris, Les Belles Lettres, 1941 (Collection des universités de France)
; Platon
: ''Les Lois'', texte établi et traduit par Édouard des Places (livres I-VI, 1951) et Auguste Diès (livres VII-XII, 1956), Paris, Les Belles Lettres (Collection des universités de France)
; Aristote
: ''La Métaphysique'', trad. Jean Tricot, Paris, Vrin, 1933 (nouvelle éd. 1953)
; Aristote
: ''Physique'', trad. Pierre Pellegrin, Paris, Flammarion, 2000 (GF Flammarion)
; Aristote
: ''Traité du ciel'', trad. Jean Tricot, Paris, Vrin, 1949
; Aristote
: ''De la génération et de la corruption'', trad. Jean Tricot, Paris, Vrin, 1934
; Aristote
: ''Les Météorologiques'', trad. Jean Tricot, Paris, Vrin, 1941
; Aristote
: ''De la génération des animaux'', texte établi et traduit par Pierre Louis, Paris, Les Belles Lettres, 1961 (Collection des universités de France)
; Aristote
: ''Les Parties des animaux'', texte établi et traduit par Pierre Louis, Paris, Les Belles Lettres, 1956 (Collection des universités de France)
; Aristote
: ''Histoire des animaux'', trad. Jean Tricot, Paris, Vrin, 1957
; Aristote
: ''De l'âme'', trad. Richard Bodéüs, Paris, Flammarion, 1993 (GF Flammarion)
; Diogène Laërce
: ''Vies et doctrines des philosophes illustres'', sous la dir. de Marie-Odile Goulet-Cazé, Paris, Le Livre de Poche, 1999 (La Pochothèque)
; Théophraste
: ''De sensibus'', éd. et trad. angl. dans George M. Stratton, ''Theophrastus and the Greek Physiological Psychology before Aristotle'', Londres, Allen & Unwin, 1917
; Lucrèce
: ''De la nature'', texte établi et traduit par Alfred Ernout, Paris, Les Belles Lettres, 1920 (Collection des universités de France)
; Cicéron
: ''La Nature des dieux'', trad. Clara Auvray-Assayas, Paris, Les Belles Lettres, 2002 (La Roue à livres)
; Plutarque
: ''Vies parallèles'', trad. Anne-Marie Ozanam, éd. François Hartog, Paris, Gallimard, 2001 (Quarto)
; Xénophon
: ''Mémorables'', texte établi et traduit par Louis-André Dorion et Michele Bandini, Paris, Les Belles Lettres, 2000-2011 (Collection des universités de France)
=== Études sur Anaxagore ===
; Althoff, Jochen
: « Presocratic Discourse in Poetry and Prose: The Case of Empedocles and Anaxagoras », ''Studies in History and Philosophy of Science'', vol. 43, 2012, p. 293-299
; Arsenijević, Miloš, Popović, Saša & Vuletić, Miloš
: « Anaxagoras, the Thoroughgoing Infinitist: The Relation between his Teachings on Multitude and on Heterogeneity », ''European Journal of Analytic Philosophy'', vol. 15, 2019, p. 35-70
; Bargrave-Weaver, D.
: « The Cosmogony of Anaxagoras », ''Phronesis'', vol. 4, 1959, p. 77-91
; Barnes, Jonathan
: ''The Presocratic Philosophers'', Londres, Routledge, 1979, 2 vol. ; éd. révisée en un volume, 1982
; Betegh, Gábor
: « Archelaus on Cosmogony and the Origins of Social Institutions », ''Oxford Studies in Ancient Philosophy'', vol. 51, 2016, p. 1-40
; Bicknell, P. J.
: « Did Anaxagoras Observe a Sunspot in 467 B.C.? », ''Isis'', vol. 59, 1968, p. 87-90
; Brennan, Tad
: « The Text of Anaxagoras Fragment DK 59 B22 », ''American Journal of Philology'', vol. 116, 1995, p. 533-537
; Bröcker, Walter
: « Die Lehre des Anaxagoras », ''Kant-Studien'', vol. 42, 1942-1943, p. 176-189
; Burnet, John
: ''Early Greek Philosophy'', 4{{e}} éd., Londres, A. & C. Black, 1930
: [Le chapitre VI reprend celui de la 3{{e}} éd., 1920]
; Cleve, Felix M.
: ''The Philosophy of Anaxagoras: An Attempt at Reconstruction'', New York, King's Crown Press, 1949 ; nouvelle éd., ''The Philosophy of Anaxagoras, as Reconstructed by Felix M. Cleve'', La Haye, Martinus Nijhoff, 1973
; Cornford, F. M.
: « Anaxagoras' Theory of Matter », ''Classical Quarterly'', vol. 24, 1930, p. 14-30 et 83-95
; Couprie, Dirk L.
: « Anaxagoras on the Milky Way and Lunar Eclipses », ''Hyperboreus'', vol. 23, 2017, p. 181-207
; Couprie, Dirk L.
: « Anaxagoras on the Light and Phases of the Moon », ''Hyperboreus'', vol. 24, 2018, p. 12-39
; Curd, Patricia
: ''The Legacy of Parmenides: Eleatic Monism and Later Presocratic Thought'', Princeton, Princeton University Press, 1998 ; rééd. Las Vegas, Parmenides Publishing, 2004
; Curd, Patricia & Sisko, John
: « Anaxagoras », ''Stanford Encyclopedia of Philosophy'', 1{{re}} publication 2007, révision substantielle 2026
; Davison, J. A.
: « Protagoras, Democritus, and Anaxagoras », ''Classical Quarterly'', n.s. vol. 3, 1953, p. 33-45
; DeFilippo, Joseph G.
: « Reply to André Laks on Anaxagoras' ΝΟΥΣ », ''Southern Journal of Philosophy'', vol. 31, suppl., 1993, p. 39-48
; Deichgräber, Karl
: « Hymnische Elemente in der philosophischen Prosa der Vorsokratiker », ''Philologus'', vol. 88, 1933, p. 347-361
; Dover, K. J.
: « The Freedom of the Intellectual in Greek Society », ''Talanta'', vol. 7, 1975, p. 24-54
; Drozdek, Adam
: « Anaxagoras and the Everything in Everything Principle », ''Hermes'', vol. 133, 2005, p. 163-177
; Erhard, Hubert
: « Anaxagoras als Biologe », ''Sudhoffs Archiv für Geschichte der Medizin und der Naturwissenschaften'', vol. 35, 1942, p. 117-140
; Freeman, Kathleen
: « Anaxagoras », ''Greece & Rome'', vol. 4, 1935, p. 65-75
; Fritz, Kurt von
: « Der ΝΟΥΣ des Anaxagoras », ''Archiv für Begriffsgeschichte'', vol. 9, 1964, p. 87-102 ; repris dans ''Grundprobleme der Geschichte der antiken Wissenschaft'', Berlin et New York, De Gruyter, 1971, p. 576-593
; Furley, David J.
: « Anaxagoras in Response to Parmenides », ''Canadian Journal of Philosophy'', suppl. vol. 2, 1976, p. 61-85 ; repris dans ''Cosmic Problems'', Cambridge, Cambridge University Press, 1989, p. 47-65
; Furley, David J.
: « Anaxagoras, Plato and the Naming of Parts », dans Victor Caston et Daniel W. Graham (éd.), ''Presocratic Philosophy: Essays in Honour of Alexander Mourelatos'', Aldershot, Ashgate, 2002, p. 119-126
; Furth, Montgomery
: « A “Philosophical Hero”? Anaxagoras and the Eleatics », ''Oxford Studies in Ancient Philosophy'', vol. 9, 1991, p. 95-129
; Geffcken, Johannes
: « Die Asebeia des Anaxagoras », ''Hermes'', vol. 42, 1907, p. 127-133
; Gershenson, Daniel E. & Greenberg, Daniel A.
: ''Anaxagoras and the Birth of Scientific Method'', New York et Londres, Blaisdell, 1964 (A Blaisdell Book in the History of Science)
: [Version abrégée de la première partie de l'ouvrage suivant ; à utiliser avec précaution : la thèse d'une « naissance de la méthode scientifique » doit être nuancée]
; Gershenson, Daniel E. & Greenberg, Daniel A.
: ''Anaxagoras and the Birth of Physics'', New York et Londres, Blaisdell, 1964, xxv-538 p. (A History of Physics, série I)
: [La préface de l'édition abrégée indique 1963]
: [La préface de l'édition abrégée indique 1963]
; Gigon, Olof
: « Zu Anaxagoras », ''Philologus'', vol. 91, 1936-1937, p. 1-41 ; repris dans ''Studien zur antiken Philosophie'', Berlin et New York, De Gruyter, 1972
; Graham, Daniel W.
: « The Postulates of Anaxagoras », ''Apeiron'', vol. 27, 1994, p. 77-121
; Graham, Daniel W.
: « Empedocles and Anaxagoras: Responses to Parmenides », dans A. A. Long (éd.), ''The Cambridge Companion to Early Greek Philosophy'', Cambridge, Cambridge University Press, 1999, p. 159-180
; Graham, Daniel W.
: « Was Anaxagoras a Reductionist? », ''Ancient Philosophy'', vol. 24, 2004, p. 1-18
; Graham, Daniel W.
: « Anaxagoras and the Comet », ''Ancient Philosophy'', vol. 33, 2013, p. 1-18
; Graham, Daniel W. & Hintz, Eric
: « Anaxagoras and the Solar Eclipse of 478 BC », ''Apeiron'', vol. 40, 2007, p. 319-344
; Inwood, Brad
: « Anaxagoras and Infinite Divisibility », ''Illinois Classical Studies'', vol. 11, 1986, p. 17-33
; Janko, Richard
: « Eclipse and Plague: Themistocles, Pericles, Anaxagoras and the Athenians' War on Science », ''Journal of Hellenic Studies'', vol. 140, 2020, p. 213-237
; Kember, Owen
: « Anaxagoras' Theory of Sex Differentiation and Heredity », ''Phronesis'', vol. 18, 1973, p. 1-14
; Kerferd, George B.
: « Anaxagoras and the Concept of Matter before Aristotle », ''Bulletin of the John Rylands Library'', vol. 52, 1969, p. 129-143 ; repris dans Mourelatos (éd.), 1974, p. 489-503
; Kingsley, Peter
: « Notes on Air: Four Questions of Meaning in Empedocles and Anaxagoras », ''Classical Quarterly'', n.s. vol. 45, 1995, p. 26-29
; Laks, André
: « Mind's Crisis: On Anaxagoras' ''Nous'' », ''Southern Journal of Philosophy'', vol. 31, suppl., 1993, p. 19-38
; Lesher, James H.
: « Mind's Knowledge and Powers of Control in Anaxagoras DK B12 », ''Phronesis'', vol. 40, 1995, p. 125-142
; Lewis, Eric
: « Anaxagoras and the Seeds of a Physical Theory », ''Apeiron'', vol. 33, 2000, p. 1-23
; Mann, William E.
: « Anaxagoras and the ''Homoiomerē'' », ''Phronesis'', vol. 25, 1980, p. 228-249
; Mansfeld, Jaap
: « The Chronology of Anaxagoras' Athenian Period and the Date of His Trial », I, ''Mnemosyne'', vol. 32, 1979, p. 39-69 ; II, ''Mnemosyne'', vol. 33, 1980, p. 17-95
; Mansfeld, Jaap
: « Anaxagoras' Other World », ''Phronesis'', vol. 25, 1980, p. 1-4
; Marmodoro, Anna
: « Anaxagoras's Qualitative Gunk », ''British Journal for the History of Philosophy'', vol. 23, 2015, p. 402-422
; Marmodoro, Anna
: ''Everything in Everything: Anaxagoras's Metaphysics'', New York, Oxford University Press, 2017
; O'Brien, Denis
: « The Relation of Anaxagoras and Empedocles », ''Journal of Hellenic Studies'', vol. 88, 1968, p. 93-113
; O'Brien, Denis
: « Derived Light and Eclipses in the Fifth Century », ''Journal of Hellenic Studies'', vol. 88, 1968, p. 114-127
; Paxson, Thomas D., Jr.
: « The Holism of Anaxagoras », ''Apeiron'', vol. 17, 1983, p. 85-91
; Peck, Arthur L.
: « Anaxagoras and the Parts », ''Classical Quarterly'', vol. 20, 1926, p. 57-71
; Peck, Arthur L.
: « Anaxagoras: Predication as a Problem in Physics », ''Classical Quarterly'', vol. 25, 1931, p. 27-37 et 112-120
; Pinto, Rhodes
: « ''Nous'', Motion, and Teleology in Anaxagoras », ''Oxford Studies in Ancient Philosophy'', vol. 52, 2017, p. 1-32
; Potts, Ronald
: « Anaxagoras' Cosmogony », ''Apeiron'', vol. 18, 1984, p. 90-96
; Raven, J. E.
: « The Basis of Anaxagoras' Cosmology », ''Classical Quarterly'', n.s. vol. 4, 1954, p. 123-137
; Reesor, Margaret E.
: « The Meaning of Anaxagoras », ''Classical Philology'', vol. 55, 1960, p. 1-8
; Rösler, Wolfgang
: « ΟΜΟΥ ΧΡΗΜΑΤΑ ΠΑΝΤΑ ΗΝ », ''Hermes'', vol. 99, 1971, p. 246-248
; Schofield, Malcolm
: « Doxographica Anaxagorea », ''Hermes'', vol. 103, 1975, p. 1-24
; Schofield, Malcolm
: ''An Essay on Anaxagoras'', Cambridge, Cambridge University Press, 1980 (Cambridge Classical Studies)
; Sider, David
: « Anaxagoras on the Size of the Sun », ''Classical Philology'', vol. 68, 1973, p. 128-129
; Sider, David
: « A Note on Anaxagoras, Fr. 1 », ''Archiv für Geschichte der Philosophie'', vol. 55, 1973, p. 249-251
; Sider, David
: « Anaxagoras Fr. 14 DK », ''Hermes'', vol. 102, 1974, p. 365-367
; Sisko, John E.
: « Anaxagoras' Parmenidean Cosmology: Worlds within Worlds within the One », ''Apeiron'', vol. 36, 2003, p. 87-114
; Sisko, John E.
: « Anaxagoras betwixt Parmenides and Plato », ''Philosophy Compass'', vol. 5, 2010, p. 432-442
; Sisko, John E.
: « Anaxagoras on Matter, Motion, and Multiple Worlds », ''Philosophy Compass'', vol. 5, 2010, p. 443-454
; Solmsen, Friedrich
: « Anaxagoras B 19 Diels-Kranz », ''Hermes'', vol. 91, 1963, p. 251-252
; Stokes, M. C.
: « On Anaxagoras. Part I: Anaxagoras' Theory of Matter », ''Archiv für Geschichte der Philosophie'', vol. 47, 1965, p. 1-19 ; « Part II: The Order of Cosmogony », ibid., p. 217-250
; Strang, Colin
: « The Physical Theory of Anaxagoras », ''Archiv für Geschichte der Philosophie'', vol. 45, 1963, p. 101-118 ; repris dans Furley et Allen (éd.), vol. II, 1975, p. 361-380
; Tannery, Paul
: « La théorie de la matière d'Anaxagore », ''Revue philosophique'', vol. 22, 1886, p. 255-274 ; repris dans ''Pour l'histoire de la science hellène'', Paris, Alcan, 1887
; Taylor, A. E.
: « On the Date of the Trial of Anaxagoras », ''Classical Quarterly'', vol. 11, 1917, p. 81-87
; Teodorsson, Sven-Tage
: ''Anaxagoras' Theory of Matter'', Göteborg, Acta Universitatis Gothoburgensis, 1982 (Studia Graeca et Latina Gothoburgensia, 42)
; Theodossiou, Evangelos Th., Niarchos, P. G., Manimanis, V. N. & Orchiston, Wayne
: « The Fall of a Meteorite at Aegos Potami in 467/466 BC », ''Journal of Astronomical History and Heritage'', vol. 5, 2002, p. 135-140
; Vlastos, Gregory
: « The Physical Theory of Anaxagoras », ''Philosophical Review'', vol. 59, 1950, p. 31-57 ; repris dans Furley et Allen (éd.), vol. II, 1975, p. 323-353, et dans ''Studies in Greek Philosophy'', vol. I, éd. Daniel W. Graham, Princeton, Princeton University Press, 1995, p. 303-327
; Warren, James
: « Anaxagoras on Perception, Pleasure, and Pain », ''Oxford Studies in Ancient Philosophy'', vol. 33, 2007, p. 19-54
; Wasserstein, A.
: « A Note on Fragment 12 of Anaxagoras », ''Classical Review'', n.s. vol. 10, 1960, p. 4-5
; Weisinger, Kenneth D.
: « A Note on Homunculus, Thales, and Anaxagoras », ''Monatshefte'', vol. 64, 1972, p. 237-246
; Woodbury, Leonard
: « Anaxagoras and Athens », ''Phoenix'', vol. 35, 1981, p. 295-315
; Woolf, N. J.
: « Anaxagoras and the Scientist/Laity Interaction », ''Vistas in Astronomy'', vol. 39, 1995, p. 699-709
=== Comptes rendus ===
; Hall, Marie Boas
: « Anaxagoras Revisited », compte rendu de Gershenson et Greenberg, 1964, ''Nature'', vol. 204, 1964 (supplément du 5 décembre), p. 968
; Louguet, Claire
: Compte rendu de Curd, ''Anaxagoras of Clazomenae'', 2007, ''Classical Review'', n.s. vol. 59, 2009, p. 23-24
; Naddaf, Gerard
: Compte rendu de Graham, ''Science before Socrates'', 2013, ''Mind'', vol. 125, 2016, p. 945-952
; Rawlins, F. I. G.
: « Anaxagoras: Philosopher and Artist », compte rendu de Cleve, 1949, ''Nature'', vol. 166, 1950, p. 2-3
; Sider, David
: Compte rendu de Teodorsson, ''Anaxagoras' Theory of Matter'', 1982, ''Classical Journal'', vol. 80, 1984, p. 71-73
=== Présocratiques, sciences et contexte ===
; Betegh, Gábor
: ''The Derveni Papyrus: Cosmology, Theology and Interpretation'', Cambridge, Cambridge University Press, 2004
; Curd, Patricia
: « Presocratic Philosophy », ''Stanford Encyclopedia of Philosophy'', 1{{re}} publication 2007, révision substantielle 2020
; Dover, Kenneth
: ''Aristophanes: Clouds'', éd. avec introduction et commentaire, Oxford, Clarendon Press, 1968
; Fränkel, Hermann
: ''Wege und Formen frühgriechischen Denkens'', 2{{e}} éd., Munich, Beck, 1960
; Furley, David J. & Allen, R. E. (éd.)
: ''Studies in Presocratic Philosophy'', Londres, Routledge and Kegan Paul, 1970-1975, 2 vol.
; Graham, Daniel W.
: « La lumière de la lune dans la pensée grecque archaïque », dans André Laks et Claire Louguet (éd.), ''Qu'est-ce que la philosophie présocratique ?'', Lille, Presses universitaires du Septentrion, 2002, p. 351-380
; Graham, Daniel W.
: ''Explaining the Cosmos: The Ionian Tradition of Scientific Philosophy'', Princeton, Princeton University Press, 2006
; Graham, Daniel W.
: ''Science before Socrates: Parmenides, Anaxagoras, and the New Astronomy'', New York, Oxford University Press, 2013
; Guthrie, W. K. C.
: ''A History of Greek Philosophy'', vol. II : ''The Presocratic Tradition from Parmenides to Democritus'', Cambridge, Cambridge University Press, 1965
; Lloyd, G. E. R.
: ''Polarity and Analogy: Two Types of Argumentation in Early Greek Thought'', Cambridge, Cambridge University Press, 1966
; Lloyd, G. E. R.
: ''Early Greek Science: Thales to Aristotle'', Londres, Chatto & Windus, 1970
; Mourelatos, A. P. D. (éd.)
: ''The Pre-Socratics: A Collection of Critical Essays'', Garden City, Anchor Press, 1974
; Norden, Eduard
: ''Agnostos Theos. Untersuchungen zur Formengeschichte religiöser Rede'', Leipzig, Teubner, 1913 (4{{e}} tirage, Stuttgart, 1956)
; Owen, G. E. L.
: « Eleatic Questions », ''Classical Quarterly'', n.s. vol. 10, 1960, p. 84-102
; Owen, G. E. L.
: « Plato and Parmenides on the Timeless Present », ''The Monist'', vol. 50, 1966, p. 317-340 ; repris dans Mourelatos (éd.), 1974, p. 271-292
; Sedley, David
: ''Creationism and Its Critics in Antiquity'', Berkeley, University of California Press, 2007
; Vassallo, Christian
: ''The Presocratics at Herculaneum: A Study of Early Greek Philosophy in the Epicurean Tradition'', Berlin et Boston, De Gruyter, 2021 (Studia Praesocratica, 11)
=== Influence et réception ===
; Brisson, Luc
: ''Le Même et l'Autre dans la structure ontologique du Timée de Platon'', Paris, Klincksieck, 1974
; Chalmers, Alan
: « Atomism from the 17th to the 20th Century », ''Stanford Encyclopedia of Philosophy'', 1{{re}} publication 2005
; Long, A. A. & Sedley, D. N.
: ''The Hellenistic Philosophers'', vol. I, Cambridge, Cambridge University Press, 1987
; Morrow, Glenn R.
: « Necessity and Persuasion in Plato's ''Timaeus'' », ''Philosophical Review'', vol. 59, 1950, p. 147-163
; Sedley, David
: ''Lucretius and the Transformation of Greek Wisdom'', Cambridge, Cambridge University Press, 1998
; White, Michael J.
: « Stoic Natural Philosophy (Physics and Cosmology) », dans Brad Inwood (éd.), ''The Cambridge Companion to the Stoics'', Cambridge, Cambridge University Press, 2003, p. 124-152
=== Dictionnaires et encyclopédies ===
; Goulet, Richard (dir.)
: ''Dictionnaire des philosophes antiques'', vol. I, Paris, CNRS Éditions, 1989
{{autocat}}
[[Catégorie:Philosophe]]
{{DEFAULTSORT:Anaxagore}}
[[Catégorie:Présocratiques]]
m2q19mfc93yue02e4he9fmb3mw4pgcn
773221
773220
2026-09-26T08:45:46Z
PandaMystique
119061
773221
wikitext
text/x-wiki
{{DicoPhilo|Anaxagore de Clazomènes|lecture=oui}}
== Vie et contexte historique ==
{{wikisource|Auteur:Anaxagore_de_Clazomènes|Anaxagore de Clazomènes}}
Les données biographiques concernant Anaxagore sont, selon l'expression de Patricia Curd, « confuses et déroutantes » (''confused and confusing''), et les anecdotes transmises par la tradition antique doivent être abordées avec prudence<ref>Patricia Curd, ''Anaxagoras of Clazomenae: Fragments and Testimonia'', Toronto, University of Toronto Press, 2007, p. 129-136, notamment p. 130.</ref>. Une grande partie des récits biographiques détaillés est transmise par des auteurs de l'époque impériale ou tardive, notamment Plutarque, Valère Maxime et Diogène Laërce, qui reprennent eux-mêmes des traditions antérieures attribuées à Démétrios de Phalère, Apollodore, Sotion, Satyros ou Hermippe<ref>Curd, 2007, p. 78-92 et 129-136.</ref>.
Anaxagore (en grec ancien Ἀναξαγόρας) naît vers 500 av. J.-C. à Clazomènes, cité grecque d'Ionie, sur la côte occidentale de l'Asie Mineure<ref>Diogène Laërce, II, 7 (DK 59 A 1) : selon Apollodore, il naquit lors de la 70{{e}} Olympiade (500-497 av. J.-C.) et mourut la première année de la 88{{e}} (428/7). La date de naissance vers 500 fait l'objet d'un large accord : Curd, 2007, p. 130 ; Malcolm Schofield, ''An Essay on Anaxagoras'', Cambridge, Cambridge University Press, 1980, p. 33 ; David Sider, ''The Fragments of Anaxagoras'', Meisenheim am Glan, Hain, 1981, p. 1-2.</ref>. Fils d'Hégésibule (certaines sources disent Eubule), il appartient, selon la tradition, à une famille aristocratique et fortunée<ref>Diogène Laërce, II, 6 (DK 59 A 1).</ref>. Diogène Laërce rapporte qu'il céda son patrimoine à ses proches et qu'il se consacra à l'étude de la nature en se tenant à l'écart des affaires publiques ; à ceux qui lui reprochaient de négliger ses biens, il aurait répondu : « Pourquoi ne vous en occupez-vous pas vous-mêmes ? »<ref>Diogène Laërce, II, 6-7 (DK 59 A 1). Platon, ''Hippias majeur'', 283a, et Plutarque, ''Vie de Périclès'', 16, évoquent aussi la négligence d'Anaxagore envers son héritage (DK 59 A 13).</ref>. Valère Maxime rapporte de son côté qu'Anaxagore, revenant d'un long voyage et trouvant ses terres à l'abandon, aurait déclaré : « Je ne serais pas sauf si elles n'avaient pas péri »<ref>Valère Maxime, ''Faits et dits mémorables'', VIII, 7, ext. 6 (DK 59 A 31). Diogène Laërce ne rapporte pas ce mot. Valère Maxime y voit une parole empreinte de sagesse.</ref>.
Ces récits, qu'ils soient authentiques ou légendaires, illustrent l'image qu'Anaxagore a laissée : celle du philosophe contemplatif, détaché des affaires domestiques et tourné vers l'étude du cosmos. À quelqu'un qui lui demandait s'il n'avait aucun souci de sa patrie, il aurait répondu, en montrant le ciel : « Tais-toi, j'ai le plus grand souci de ma patrie »<ref>Diogène Laërce, II, 7 (DK 59 A 1).</ref>. Interrogé sur l'homme le plus heureux, il aurait dit qu'aucun de ceux auxquels on songe ne mérite ce titre, et que l'homme heureux paraîtrait étrange à son interlocuteur<ref>Aristote, ''Éthique à Eudème'', I, 4, 1215b6-8 ; cf. ''Éthique à Nicomaque'', X, 9, 1179a13-16 (DK 59 A 30).</ref>. À qui lui demandait pourquoi l'on devrait choisir de naître plutôt que de ne pas être, il aurait répondu : « Pour contempler le ciel et l'ordre de l'univers entier »<ref>Aristote, ''Éthique à Eudème'', I, 5, 1216a11-14 (DK 59 A 30). Schofield (1980, p. 22-23) rapproche ces anecdotes du portrait platonicien du philosophe dans la digression du ''Théétète'' et en souligne le caractère stylisé.</ref>.
=== L'arrivée à Athènes et l'activité philosophique ===
La chronologie de la vie d'Anaxagore reste discutée, en raison des divergences entre les sources anciennes<ref>Leonard Woodbury, « Anaxagoras and Athens », ''Phoenix'', vol. 35, 1981, p. 295-315 ; Jaap Mansfeld, « The Chronology of Anaxagoras' Athenian Period and the Date of His Trial », ''Mnemosyne'', vol. 32, 1979, p. 39-69, et vol. 33, 1980, p. 17-95 ; Schofield, 1980, p. 33-35 ; Sider, 1981, p. 1-11.</ref>. Diogène Laërce rapporte, d'après Démétrios de Phalère, qu'Anaxagore commença à philosopher à Athènes à l'âge de vingt ans, sous l'archontat de Callias, et qu'il y séjourna trente ans ; il le dit aussi âgé de vingt ans lors du passage de Xerxès (480 av. J.-C.)<ref>Diogène Laërce, II, 7 (DK 59 A 1). Le nom de l'archonte pose problème : Callias correspond à 456/5, tandis que la correction « Calliadès » (480/79) s'accorde avec l'âge de vingt ans. Voir Sider, 1981, p. 2-4 ; Curd, 2007, p. 78, n. 1, et p. 131.</ref>. Une arrivée précoce, vers 480, est défendue par Leonard Woodbury et Daniel Graham ; Jaap Mansfeld retient 456/5 et un séjour de vingt ans, date qu'Olof Gigon admettait déjà et que suit Curd ; d'autres proposent des dates intermédiaires<ref>Woodbury, 1981, p. 299 et 306 ; Olof Gigon, « Zu Anaxagoras », ''Philologus'', vol. 91, 1936-1937, p. 1-41, repris dans ''Studien zur antiken Philosophie'', Berlin et New York, De Gruyter, 1972 ; Daniel W. Graham, ''Explaining the Cosmos: The Ionian Tradition of Scientific Philosophy'', Princeton, Princeton University Press, 2006 ; Mansfeld, 1979, p. 39 ; Curd, 2007, p. 131. Pour une présentation récente des hypothèses (480, 478, 464, 460, 456), voir Patricia Curd et John Sisko, « Anaxagoras », ''Stanford Encyclopedia of Philosophy'', 2007, révision substantielle 2026, § 1.</ref>. Les sources attribuent au séjour athénien une durée de vingt ou de trente ans ; selon la chronologie retenue, on le situe approximativement entre 480 et 450 ou entre 456 et 437 av. J.-C. Schofield, de son côté, place la composition du livre vers 470-460 av. J.-C.<ref>Curd, 2007, p. 129 ; Schofield, 1980, p. 33-35.</ref>. Aristote dit d'Anaxagore qu'il était antérieur à Empédocle par l'âge, mais postérieur par ses œuvres, formule dont le sens est discuté ; Denis O'Brien a soutenu, en s'appuyant sur Alcidamas, selon qui Empédocle avait été l'auditeur d'Anaxagore, que celui-ci écrivit le premier et influença Empédocle<ref>Aristote, ''Métaphysique'', A, 3, 984a11-13 (DK 59 A 43) ; Diogène Laërce, VIII, 56 ; Denis O'Brien, « The Relation of Anaxagoras and Empedocles », ''Journal of Hellenic Studies'', vol. 88, 1968, p. 93-113, ici p. 93-94.</ref>.
Quelle que soit la date de son arrivée, Anaxagore est généralement présenté comme le premier philosophe présocratique connu pour s'être établi à Athènes ; Clément d'Alexandrie lui attribue le transfert en Attique de l'enseignement ionien issu d'Anaximène<ref>Clément d'Alexandrie, ''Stromates'', I, 63 (DK 59 A 7) ; Curd, 2007, p. 129 et n. 1. La filiation personnelle avec Anaximène est chronologiquement peu vraisemblable.</ref>.
La tradition biographique, dominée par le récit de Plutarque, lie Anaxagore à Périclès<ref>Plutarque, ''Vie de Périclès'', 4-6, 16, 32 (DK 59 A 15-17) ; Platon, ''Phèdre'', 269e-270a (DK 59 A 15) ; Isocrate, ''Sur l'échange'', 235 (DK 59 A 15), qui fait de Périclès le disciple d'Anaxagore et de Damon ; Diodore de Sicile, XII, 39 (DK 59 A 17), qui appelle Anaxagore « le maître de Périclès ».</ref>. Curd juge cette amitié « bien établie », mais Schofield invite à ne pas prendre trop au sérieux le titre d'« élève » donné à Périclès : il en retient seulement la vraisemblance d'une influence exercée dans la jeunesse de l'homme d'État<ref>Curd, 2007, p. 132 ; Schofield, 1980, p. 34. Sider (1981, p. 3) rappelle que ce genre de récit reflète souvent la conviction qu'un homme plus jeune a appris d'un aîné.</ref>. Le ''Phèdre'' associe explicitement la fréquentation d'Anaxagore à l'élévation intellectuelle et oratoire de Périclès ; Schofield traite cette construction biographique avec prudence<ref>Platon, ''Phèdre'', 269e-270a ; Schofield, 1980, p. 22-23 et 34.</ref>.
Parmi les auditeurs d'Anaxagore, la tradition mentionne le poète Euripide<ref>Diogène Laërce, II, 10 (DK 59 A 1) ; Alexandre d'Étolie, cité par Aulu-Gelle, ''Nuits attiques'', XV, 20 (DK 59 A 21). Curd (2007, p. 132-133, n. 13) souligne que la présence d'échos anaxagoréens chez Euripide n'implique pas une relation formelle de maître à élève.</ref> et Archélaos, qui aurait ensuite été le maître de Socrate<ref>Diogène Laërce, II, 16 (DK 60 A 1) ; Curd, 2007, p. 134 et n. 18.</ref>. La question de savoir si Socrate a rencontré Anaxagore reste ouverte. Dans le ''Phédon'', Socrate dit avoir entendu quelqu'un lire le livre d'Anaxagore, puis l'avoir lu lui-même<ref>Platon, ''Phédon'', 97b-99d (DK 59 A 47). Platon ne nomme pas le lecteur ; l'hypothèse selon laquelle il s'agirait d'Archélaos est une conjecture moderne (Curd, 2007, p. 134 ; Burnet la formule avec un point d'interrogation).</ref>. Certains en ont conclu qu'Anaxagore avait quitté Athènes avant que Socrate ne s'intéresse à la philosophie ; Curd juge cet argument du silence peu probant<ref>Curd, 2007, p. 134-136 ; en sens contraire, Woodbury, 1981, p. 297, et Schofield, 1980, p. 34-35.</ref>. Dans l’''Apologie'', Socrate dit que les livres d'Anaxagore pouvaient s'acquérir pour une drachme au plus « à l'orchestra » ; le passage présente donc ses écrits comme accessibles à Athènes, tout en laissant discutée la nature exacte de cette transaction<ref>Platon, ''Apologie de Socrate'', 26d-e (DK 59 A 35) ; Curd, 2007, p. 92, n. 12.</ref>.
=== L'œuvre écrite ===
Les témoignages antiques et l'étude de la transmission favorisent l'hypothèse qu'Anaxagore n'a publié qu'un seul ouvrage en prose ionienne, désigné diversement dans les sources : Περὶ φύσεως, τὰ Φυσικά, ou simplement son « ouvrage » ou ses « livres » ; Simplicius cite ce qu'il appelle le « premier livre »<ref>Diogène Laërce, I, 16 (DK 59 A 37), range Anaxagore parmi les auteurs d'un seul livre, dans une liste peu sûre ; Simplicius, ''Commentaire sur la Physique d'Aristote'', 34, 29 et 155, 26 ; Sider, 1981, p. 11-13, qui conclut en faveur d'un ouvrage unique.</ref>. Platon emploie le pluriel τὰ Ἀναξαγόρου βιβλία dans l’''Apologie''<ref>Platon, ''Apologie'', 26d.</ref> : Burnet y voyait l'indice d'un ouvrage occupant plusieurs rouleaux, ce que Raven contestait ; pour Sider, ces « livres » désignent plutôt les parties d'un même ouvrage<ref>Sider, 1981, p. 12-13 ; John Burnet, ''Early Greek Philosophy'', 4{{e}} éd., Londres, A. & C. Black, 1930, chap. VI ; Sven-Tage Teodorsson, ''Anaxagoras' Theory of Matter'', Göteborg, Acta Universitatis Gothoburgensis, 1982, p. 10.</ref>. Les autres écrits qu'on lui attribue (sur la quadrature du cercle, sur la perspective, un recueil de problèmes) ne peuvent être établis comme des ouvrages indépendants, et plusieurs de ces attributions sont douteuses ; le traité de perspective pourrait correspondre à un sujet abordé par Anaxagore plutôt qu'à un livre distinct<ref>DK 59 A 38-40 ; Sider, 1981, p. 11-13 ; Teodorsson, 1982, p. 10 ; Burnet, 1930, chap. VI.</ref>. Diogène Laërce rapporte encore qu'Anaxagore fut le premier à publier un livre accompagné de figures<ref>Diogène Laërce, II, 11 (DK 59 A 1).</ref>.
Diogène Laërce qualifie son style d'agréable et d'élevé<ref>Diogène Laërce, II, 6 (DK 59 A 1).</ref>. Schofield a analysé la texture de cette prose archaïque : syntaxe paratactique, répétitions, et, dans le fragment B12, un style de « prédication solennelle » qui accumule les attributs du Noûs à la manière d'un hymne<ref>Schofield, 1980, p. 6-9, qui s'appuie sur Karl Deichgräber, « Hymnische Elemente in der philosophischen Prosa der Vorsokratiker », ''Philologus'', vol. 88, 1933, p. 347-361, et renvoie à Eduard Norden et Hermann Fränkel.</ref>. Contrairement à Parménide et à Empédocle, qui écrivent en hexamètres, Anaxagore s'inscrit dans la tradition de la prose ionienne<ref>Jochen Althoff, « Presocratic Discourse in Poetry and Prose: The Case of Empedocles and Anaxagoras », ''Studies in History and Philosophy of Science'', vol. 43, 2012, p. 293-299.</ref>. Il subsiste de ce traité une vingtaine de fragments, conservés pour la plupart par Simplicius de Cilicie (VI{{e}} siècle ap. J.-C.) dans ses commentaires sur Aristote<ref>Édition de référence : Hermann Diels et Walther Kranz, ''Die Fragmente der Vorsokratiker'', 6{{e}} éd., Berlin, Weidmann, 1951-1952, vol. II, p. 5-44 (chapitre 59, témoignages A et fragments B). Édition plus récente, avec une nouvelle numérotation (Anaxagore y porte le numéro 25) : André Laks et Glenn W. Most, ''Early Greek Philosophy'', 9 vol., Cambridge (Mass.), Harvard University Press, 2016 ; version française : ''Les Débuts de la philosophie'', Paris, Fayard, 2016. Voir aussi Curd, 2007, et Sider, 1981 (2{{e}} éd. 2005). Le fragment B20 est tenu pour inauthentique depuis Sider (Claire Louguet, compte rendu de Curd, 2007, ''Classical Review'', n.s. vol. 59, 2009, p. 23-24).</ref>. Les citations de Simplicius semblent limitées au premier livre, consacré aux principes généraux<ref>Burnet, 1930, chap. VI ; Teodorsson, 1982, p. 10. Schofield (1980, p. 159, n. 36) doute que Simplicius ait disposé du livre entier plutôt que d'extraits (cf. Curd, 2007, p. 165, n. 25).</ref>.
=== Le procès et l'exil ===
Les circonstances du départ d'Anaxagore d'Athènes sont particulièrement controversées. Diogène Laërce rapporte plusieurs versions de son procès<ref>Diogène Laërce, II, 12-14 (DK 59 A 1).</ref>. Selon Sotion, Anaxagore fut accusé d'impiété (ἀσέβεια) par Cléon pour avoir soutenu que le soleil était une masse de métal incandescent ; défendu par Périclès, il fut condamné à une amende de cinq talents et à l'exil<ref>Sotion, dans Diogène Laërce, II, 12.</ref>. Selon Satyros, l'accusateur fut Thucydide, fils de Mélésias, adversaire politique de Périclès, et l'accusation porta sur l'impiété et le médisme ; Anaxagore aurait été condamné à mort par contumace<ref>Satyros, dans Diogène Laërce, II, 12.</ref>. Hermippe le dit emprisonné en attente d'exécution puis libéré à la demande de Périclès ; Hiéronymos rapporte que Périclès le présenta au tribunal affaibli par la maladie, si bien qu'il fut acquitté par pitié<ref>Hermippe et Hiéronymos, dans Diogène Laërce, II, 13-14.</ref>.
Plutarque rapporte qu'un certain Diopeithès fit voter un décret autorisant les poursuites contre ceux qui ne reconnaissaient pas les dieux ou enseignaient des doctrines sur les phénomènes célestes, afin de jeter le soupçon sur Périclès à travers Anaxagore ; Périclès, craignant pour Anaxagore, l'éloigna de la cité<ref>Plutarque, ''Vie de Périclès'', 32, 1-5 (DK 59 A 17). Plutarque ne précise pas le contenu des doctrines incriminées. La thèse du soleil-pierre et de la lune-terre est mentionnée par Platon, ''Apologie'', 26d (DK 59 A 35), où Mélétos attribue ces opinions à Socrate. Plutarque (''Vie de Nicias'', 23 = DK 59 A 18) dit aussi que Périclès eut peine à tirer Anaxagore de prison.</ref>. Johannes Geffcken jugeait le décret de Diopeithès historiquement fondé et estimait que l'accusation visait l'ensemble de la « météorologie » d'Anaxagore, notamment son explication naturelle de la foudre, plus que la seule thèse du soleil<ref>Johannes Geffcken, « Die Asebeia des Anaxagoras », ''Hermes'', vol. 42, 1907, p. 127-133, ici p. 133.</ref>.
L'historicité du procès a été mise en doute, notamment par K. J. Dover, pour qui aucune des sources anciennes ne savait réellement ce qui était arrivé à Anaxagore ; J. A. Davison a au contraire reconstitué une vie comportant deux procès, l'un vers 456/5, l'autre vers 433-430, ce qui l'oblige à supposer une amnistie dont rien ne témoigne<ref>K. J. Dover, « The Freedom of the Intellectual in Greek Society », ''Talanta'', vol. 7, 1975, p. 24-54 (surtout p. 27-32), cité par Schofield, 1980, p. 34 et n. 74 ; J. A. Davison, « Protagoras, Democritus, and Anaxagoras », ''Classical Quarterly'', n.s. vol. 3, 1953, p. 33-45 (surtout p. 39-45), suivi par Guthrie et Russell Meiggs ; critique de cette reconstruction chez Woodbury, 1981, p. 303 (en note). Gershenson et Greenberg, dans ''Anaxagoras and the Birth of Physics'' (1964), nient également le procès (Teodorsson, 1982, p. 7, n. 2).</ref>. Curd estime néanmoins qu'il y a peu de raisons de douter de la tradition sur ce point<ref>Curd, 2007, p. 136.</ref>. La date reste incertaine : vers 450 selon A. E. Taylor et Burnet, au milieu du siècle selon Woodbury, qui rattache l'accusation portée par Thucydide, fils de Mélésias, aux luttes politiques de cette période, en 437/6 selon Mansfeld, vers 434 selon Sider, vers 430 selon Richard Janko<ref>A. E. Taylor, « On the Date of the Trial of Anaxagoras », ''Classical Quarterly'', vol. 11, 1917, p. 81-87 ; Burnet, 1930, chap. VI ; Woodbury, 1981, p. 315 ; Mansfeld, 1979, p. 39 ; Sider, 1981, p. 1-11 ; Richard Janko, « Eclipse and Plague: Themistocles, Pericles, Anaxagoras and the Athenians' War on Science », ''Journal of Hellenic Studies'', vol. 140, 2020, p. 213-237. Pour la synthèse, voir Curd et Sisko, ''SEP'', 2026, § 1.</ref>.
Anaxagore se retira ensuite à Lampsaque, sur l'Hellespont (l'actuel détroit des Dardanelles), où il fut honoré<ref>Diogène Laërce, II, 14-15 (DK 59 A 1) ; Alcidamas, cité par Aristote, ''Rhétorique'', II, 23, 1398b15-16 (DK 59 A 23).</ref>. Il y mourut en 428/7 av. J.-C., à soixante-douze ans selon une tradition que rapporte Diogène Laërce<ref>Diogène Laërce, II, 7 (DK 59 A 1) : l'âge de soixante-douze ans vient d'une tradition anonyme (« on dit »), la date de la mort d'Apollodore ; voir la traduction du passage chez Mansfeld, 1979, p. 41.</ref>. Invité par les magistrats de la cité à exprimer un vœu, il aurait demandé que les enfants aient congé chaque année dans le mois de sa mort, usage encore observé au temps de Diogène Laërce<ref>Diogène Laërce, II, 14 (DK 59 A 1).</ref>. Élien rapporte qu'un autel lui fut élevé, portant les inscriptions « Intellect » (Νοῦς) et « Vérité » (Ἀλήθεια)<ref>Élien, ''Histoire variée'', VIII, 19 (DK 59 A 24). Le texte peut signifier soit un autel portant une inscription sur chaque face, soit un autel dédié aux deux (Curd, 2007, p. 88, n. 10).</ref>. L'épitaphe gravée sur sa tombe nous est parvenue : « Ci-gît Anaxagore, qui atteignit le plus lointain terme de la vérité sur le cosmos céleste »<ref>Diogène Laërce, II, 15 (DK 59 A 1) ; Élien, ''Histoire variée'', VIII, 19 (DK 59 A 24). Alcidamas, au IV{{e}} siècle, atteste que les Lampsacéniens l'honoraient encore (DK 59 A 23) ; Gershenson et Greenberg (1964, p. 4) rappellent que sa mémoire fut honorée pendant plus d'un siècle.</ref>. Cicéron rapporte enfin qu'à des amis qui lui demandaient, à Lampsaque, s'il voulait être ramené à Clazomènes, il aurait répondu que le chemin des Enfers est le même de partout<ref>Cicéron, ''Tusculanes'', I, 43, 104 (DK 59 A 34a) ; cf. Diogène Laërce, II, 11.</ref>.
=== Portrait et anecdotes ===
Les sources anciennes conservent de nombreuses anecdotes dont l'historicité ne peut être garantie. Apprenant la mort de son fils, Anaxagore aurait dit : « Je savais que j'avais engendré un mortel »<ref>Galien, ''Des opinions d'Hippocrate et de Platon'', IV, 7 (DK 59 A 33) ; Diogène Laërce, II, 13, rapporte un mot voisin au pluriel, à propos de ses enfants.</ref>.
Dans le ''Phèdre'', Socrate attribue l'élévation de l'éloquence de Périclès à sa fréquentation d'Anaxagore, dont les discours portaient pour l'essentiel sur la nature de l'intelligence et de la déraison (νοῦ τε καὶ ἀνοίας)<ref>Platon, ''Phèdre'', 269e-270a (DK 59 A 15) ; Schofield, 1980, p. 22-23.</ref>. Ce passage contribue au portrait platonicien d'Anaxagore comme penseur du Noûs et de la recherche sur la nature. Les ''Nuées'' d'Aristophane tournent en ridicule la nouvelle science de la nature ; toutefois, selon Dover, que suit Schofield, les idées moquées dans la pièce se rattachent plus aisément à Diogène d'Apollonie qu'à Anaxagore<ref>Aristophane, ''Les Nuées'' ; Kenneth Dover, ''Aristophanes: Clouds'', Oxford, Clarendon Press, 1968, introduction ; Schofield, 1980, p. 35 et n. 83 ; Curd, 2007, p. 132, n. 11. Gábor Betegh a proposé de voir aussi dans la pièce des échos d'Archélaos (Betegh, 2016).</ref>.
=== Les dernières années à Lampsaque ===
Les témoignages attestent surtout les honneurs rendus à Anaxagore à Lampsaque, mais ils ne permettent pas de déterminer avec certitude la nature de son activité philosophique dans la cité. Schofield y reconstruit une école anaxagoréenne ; Curd souligne que nous ignorons s'il exista une école organisée au sens propre<ref>Schofield, 1980, p. 35 et n. 84 ; Sider, 1981, p. 3-4 ; Curd, 2007, p. 129, n. 1 ; cf. Diogène Laërce, II, 14-15, et Aristote, ''Rhétorique'', 1398b15 (DK 59 A 23).</ref>. Eusèbe rapporte qu'Archélaos lui aurait succédé à la tête de cette école<ref>Eusèbe, ''Préparation évangélique'', X, 14, 13 (DK 59 A 7). Cette indication s'accorde mal avec les témoignages qui situent Archélaos à Athènes ; Curd (2007, p. 134, n. 18) note que la tradition a pu confondre certains aspects de la vie des deux hommes.</ref>. Parmi ses proches, les sources mentionnent aussi Métrodore de Lampsaque, qui prolongea l'interprétation d'Homère attribuée à Anaxagore en étudiant la « physique » du poète<ref>Diogène Laërce, II, 11 (DK 59 A 1), d'après Favorinus ; DK 61. Schofield (1980, p. 149, n. 59) range Archélaos (DK 60 A 1-5) et Métrodore (DK 61 A 2, 6) parmi ses disciples.</ref>.
== Les principes métaphysiques ==
La philosophie d'Anaxagore se construit pour une large part en réponse aux exigences posées par Parménide d'Élée<ref>Curd, 2007, p. 137-142 ; Patricia Curd, ''The Legacy of Parmenides: Eleatic Monism and Later Presocratic Thought'', Princeton, Princeton University Press, 1998 (rééd. Las Vegas, Parmenides Publishing, 2004) ; Schofield, 1980, p. 5.</ref>. Dans son poème, Parménide oppose deux voies de recherche : celle « qu'il est et qu'il n'est pas possible qu'il ne soit pas » (ὅπως ἔστιν τε καὶ ὡς οὐκ ἔστι μὴ εἶναι) et celle « qu'il n'est pas et qu'il faut qu'il ne soit pas »<ref>Parménide, DK 28 B 2, 3-5 ; cf. B 6, 1-2 (ἔστι γὰρ εἶναι, μηδὲν δ' οὐκ ἔστιν, « car être est, et le néant n'est pas »).</ref>. Il en conclut que toute génération et toute corruption véritables sont impossibles, car elles supposeraient un passage de ce qui n'est pas à ce qui est, ou l'inverse<ref>Parménide, B 8, 6-21 ; cf. Aristote, ''Physique'', I, 8, 191a23-31.</ref>. Dans l'interprétation traditionnelle, Parménide conclut que ce qui est véritablement est un, continu, homogène et immobile<ref>Parménide, B 8, 22 : « Il n'est pas divisible, puisqu'il est tout entier semblable » (οὐδὲ διαιρετόν ἐστιν, ἐπεὶ πᾶν ἐστιν ὁμοῖον).</ref>. Cette lecture comme monisme numérique a toutefois été contestée, notamment par Patricia Curd, qui comprend plutôt les caractères énoncés en B8 comme les conditions auxquelles doit satisfaire toute réalité fondamentale ; John Sisko estime au contraire qu'Anaxagore construit sa physique contre un monisme numérique qu'il attribue à Parménide<ref>Curd, 1998 ; Curd, 2007, p. 140-141, qui rappelle qu'Aristote distinguait déjà l'unité selon la définition chez Parménide et l'unité selon la matière chez Mélissos (''Métaphysique'', A, 5, 986b10-11 et 19-20) ; John E. Sisko, « Anaxagoras betwixt Parmenides and Plato », ''Philosophy Compass'', vol. 5, 2010, p. 432-442, ici p. 432.</ref>.
Anaxagore accepte l'impossibilité de la génération à partir de ce qui n'est pas, mais refuse d'en conclure que la pluralité et le mouvement sont illusoires<ref>Aristote, ''Métaphysique'', A, 3, 984a11-16 (DK 59 A 43) ; ''Physique'', I, 4, 187a26-29 (DK 59 A 52).</ref>. Théophraste, cité par Simplicius, dit qu'Anaxagore, qui avait partagé la philosophie d'Anaximène, fut le premier à modifier les doctrines sur les principes et à fournir la cause qui leur manquait<ref>Théophraste, dans Simplicius, ''Commentaire sur la Physique'', 27, 2-4 (DK 59 A 41). La « cause qui manquait » désigne vraisemblablement le Noûs, cause du mouvement (Curd, 2007, p. 93, n. 13). Le lien avec Anaximène doit s'entendre au sens large d'une appartenance à la tradition ionienne (Curd, 2007, p. 129, n. 1 ; Burnet, 1930, chap. VI).</ref>. Il nie donc la génération et la destruction véritables, tout en construisant une cosmologie destinée à rendre compte de la pluralité, du mouvement et du changement apparent<ref>Curd, 2007, p. 137-142.</ref>. Schofield souligne toutefois qu'il reste un cosmologue ionien, qui intègre certaines thèses de Parménide sans adopter sa méthode argumentative<ref>Schofield, 1980, p. 5 et 26-28 ; G. E. L. Owen, « Eleatic Questions », ''Classical Quarterly'', n.s. vol. 10, 1960, p. 84-102. Sur le débat entre ceux qui voient en Anaxagore un critique de Parménide et ceux qui en font un disciple, voir d'un côté Daniel W. Graham, « Empedocles and Anaxagoras: Responses to Parmenides », dans A. A. Long (éd.), ''The Cambridge Companion to Early Greek Philosophy'', Cambridge, Cambridge University Press, 1999, p. 159-180, et de l'autre John E. Sisko, « Anaxagoras' Parmenidean Cosmology: Worlds within Worlds within the One », ''Apeiron'', vol. 36, 2003, p. 87-114 ; voir aussi Curd et Sisko, ''SEP'', 2026, § 2.</ref>. G. E. L. Owen a relevé que la première phrase du traité, « Toutes choses étaient ensemble », est « manifestement formulée comme une contradiction plate de Parménide » sur plusieurs points essentiels<ref>G. E. L. Owen, « Plato and Parmenides on the Timeless Present », ''The Monist'', vol. 50, 1966, p. 317-340, repris dans A. P. D. Mourelatos (éd.), ''The Pre-Socratics'', Garden City, Anchor Press, 1974, p. 271-292, ici p. 276-277 ; cité par Schofield, 1980, p. 64 et n. 51.</ref>.
=== Génération, mélange et dissociation ===
Le premier principe d'Anaxagore est énoncé dans le fragment B17 : « Les Grecs ne pensent pas correctement la naissance et la destruction : aucune chose ne naît ni ne périt, mais, à partir des choses qui sont, il y a mélange et dissociation. Ainsi auraient-ils raison d'appeler la naissance “mélange” et la destruction “dissociation” » (τὸ γίνεσθαι συμμίσγεσθαι καὶ τὸ ἀπόλλυσθαι διακρίνεσθαι)<ref>Anaxagore, fragment B17, cité par Simplicius, ''Commentaire sur la Physique'', 163, 18-24 (DK 59 B 17 ; LM 25 D15). Jochen Althoff compare ce fragment en prose au fragment 31 B8 d'Empédocle, qui exprime en vers une idée voisine (« Presocratic Discourse in Poetry and Prose: The Case of Empedocles and Anaxagoras », ''Studies in History and Philosophy of Science'', vol. 43, 2012, p. 293-299).</ref>. Ce principe constitue la base de sa physique et sa réponse à l'interdit parménidien. Ce qui apparaît comme génération n'est qu'un réarrangement d'ingrédients préexistants qui se mélangent (συμμίσγεται) ; ce qui paraît destruction n'est que leur dissociation (διακρίνεται)<ref>Aristote, ''Métaphysique'', A, 3, 984a13-16 (DK 59 A 43) : selon Anaxagore, presque tous les homéomères naissent et périssent seulement par agrégation et dissociation. Curd (2007, p. 145) rappelle que l'assimilation de la génération à l'altération, qu'Aristote prête à Anaxagore (''Génération et corruption'', I, 1, 314a11-13), est une interprétation aristotélicienne.</ref>. Rien ne naît du néant, rien n'y retourne : les ingrédients de la réalité existent de toute éternité et conservent leur nature propre.
Aristote explique la position d'Anaxagore par son adhésion à l'opinion commune des physiciens selon laquelle rien ne naît de ce qui n'est pas<ref>Aristote, ''Physique'', I, 4, 187a26-29 (DK 59 A 52) ; Schofield, 1980, p. 43-44.</ref>. Le principe prend une forme plus précise dans une question que la tradition attribue à Anaxagore : « Comment le cheveu pourrait-il naître de ce qui n'est pas cheveu, et la chair de ce qui n'est pas chair ? » (πῶς γὰρ ἂν ἐκ μὴ τριχὸς γένοιτο θρὶξ καὶ σὰρξ ἐκ μὴ σαρκός;)<ref>Anaxagore, fragment B10, transmis par une scholie à Grégoire de Nazianze (DK 59 B 10). L'authenticité de la formule est discutée : Schofield l'a d'abord contestée (« Doxographica Anaxagorea », ''Hermes'', vol. 103, 1975, p. 1-24), puis a admis que le scholiaste, dont la source pourrait être Eudème, en conserve peut-être les termes (Schofield, 1980, p. 133-143). La plupart des spécialistes la tiennent pour authentique (Sider ; Curd, 2007, p. 53-54).</ref>. Cette formule exprime ce que les commentateurs appellent le principe « du semblable par le semblable » : une substance ne peut provenir que de la même substance, déjà présente sous une forme non manifeste<ref>Schofield, 1980, p. 44 et 55-58, qui analyse ce principe dans la reconstruction aristotélicienne.</ref>.
=== Tout est dans tout ===
Le deuxième principe s'énonce ainsi : « En toute chose il y a une part de toute chose, sauf de l'Intellect ; et il est des choses dans lesquelles l'Intellect aussi est présent » (ἐν παντὶ παντὸς μοῖρα ἔνεστι πλὴν νοῦ, ἔστιν οἷσι δὲ καὶ νοῦς ἔνι)<ref>Anaxagore, fragment B11, cité par Simplicius, ''Commentaire sur la Physique'', 164, 23-24 (DK 59 B 11). La même thèse figure en B6 et au début de B12.</ref>. Ce principe, souvent désigné par la formule latine ''omnia in omnibus'', est la thèse la plus caractéristique et la plus déroutante de la philosophie d'Anaxagore ; Curd le nomme « principe du mélange universel »<ref>Curd, 2007, p. 179 et n. 3 ; cf. Gregory Vlastos, « The Physical Theory of Anaxagoras », ''Philosophical Review'', vol. 59, 1950, p. 31-57 ; Colin Strang, « The Physical Theory of Anaxagoras », ''Archiv für Geschichte der Philosophie'', vol. 45, 1963, p. 101-118.</ref>. Il signifie que toute portion du mélange, si petite soit-elle, contient des parts de tous les ingrédients qui existent : aucun ingrédient n'existe à l'état pur, isolé de tous les autres<ref>Anaxagore, B6 : « Puisqu'il n'est pas possible qu'il y ait un plus petit, rien ne pourrait être séparé ni venir à être par soi-même, mais, comme au commencement, maintenant aussi toutes choses sont ensemble » (ὅτε τοὐλάχιστον μὴ ἔστιν εἶναι, οὐκ ἂν δύναιτο χωρισθῆναι, οὐδ' ἂν ἐφ' ἑαυτοῦ γενέσθαι, ἀλλ' ὅπωσπερ ἀρχὴν εἶναι καὶ νῦν πάντα ὁμοῦ).</ref>. Ce qui nous apparaît comme de l'or contient, outre l'or qui y prédomine, des parts de tous les autres ingrédients<ref>Théophraste, dans Simplicius, ''Commentaire sur la Physique'', 27, 2-11 (DK 59 A 41) ; Aristote, ''Physique'', I, 4, 187a36-b7.</ref>.
La tradition doxographique rattache cette thèse aux phénomènes de la nutrition et de la croissance. Comment la chair pourrait-elle provenir du pain et de l'eau que nous consommons, si le pain et l'eau ne contenaient pas déjà de la chair ? La nourriture doit donc contenir, de manière imperceptible, toutes les substances qui composent le corps<ref>Aétius, I, 3, 5 (DK 59 A 46) ; Simplicius, ''Commentaire sur la Physique'', 460, 4-20 (DK 59 A 45). La scholie qui transmet B10 affirme que la semence contient cheveux, ongles, veines, artères, nerfs et os, imperceptibles en raison de leur petitesse et qui se séparent peu à peu au cours de la croissance.</ref>. Curd et Schofield soulignent cependant que la nutrition n'est qu'un cas, particulièrement frappant, d'une question métaphysique plus générale sur le devenir, héritée de Parménide<ref>Curd, 2007, p. 179-180 ; Schofield, 1980, p. 121 et 133-143.</ref>.
Le principe vaut aussi pour les qualités opposées. Selon la scholie à Grégoire de Nazianze, Anaxagore affirmait qu'il y a du noir dans le blanc et du blanc dans le noir, et du léger dans le lourd<ref>Scholie à Grégoire de Nazianze, contexte de DK 59 B 10 (texte et traduction dans Schofield, 1980, p. 135-136). Sextus Empiricus (''Hypotyposes pyrrhoniennes'', I, 33 = DK 59 A 97) rapporte qu'Anaxagore disait la neige noire, puisqu'elle est de l'eau congelée et que l'eau est noire.</ref>. Ces oppositions ne sont donc pas absolues : ce qui nous paraît blanc contient du noir, en proportion trop faible pour être perçue.
Cette doctrine suscite des débats depuis l'Antiquité. Aristote y voit la conséquence de l'observation selon laquelle n'importe quoi naît de n'importe quoi<ref>Aristote, ''Physique'', III, 4, 203a23-33 (DK 59 A 45) ; Schofield, 1980, p. 43-52.</ref>. Les interprètes modernes se divisent sur la nature des « parts » (μοῖραι). Pour les uns, il s'agit de particules infiniment petites (lecture déjà présente chez Lucrèce) ; pour les autres, de proportions ou de concentrations variables d'ingrédients qui se compénètrent sans structure corpusculaire<ref>Lecture particulaire : Lucrèce, ''De la nature'', I, 830-920 (DK 59 A 44) ; Vlastos, 1950 ; W. K. C. Guthrie, ''A History of Greek Philosophy'', vol. II, Cambridge, Cambridge University Press, 1965, p. 289 ; George B. Kerferd, « Anaxagoras and the Concept of Matter before Aristotle », ''Bulletin of the John Rylands Library'', vol. 52, 1969, p. 129-143 ; Sider, 1981. Lecture non particulaire : J. E. Raven, « The Basis of Anaxagoras' Cosmology », ''Classical Quarterly'', n.s. vol. 4, 1954, p. 123-137, ici p. 128-130 ; Schofield, 1980, p. 73-79 ; Jonathan Barnes, ''The Presocratic Philosophers'', Londres, Routledge, 1982, p. 323-326 ; Brad Inwood, « Anaxagoras and Infinite Divisibility », ''Illinois Classical Studies'', vol. 11, 1986, p. 17-33, ici p. 17-18 ; Curd, 2007, p. 183-184 ; Anna Marmodoro, « Anaxagoras's Qualitative Gunk », ''British Journal for the History of Philosophy'', vol. 23, 2015, p. 402-422, et ''Everything in Everything: Anaxagoras's Metaphysics'', New York, Oxford University Press, 2017, chap. 3-4. Voir le recensement de Curd, 2007, p. 183, n. 9-10. Pour d'autres lectures du principe : Margaret E. Reesor, « The Meaning of Anaxagoras », ''Classical Philology'', vol. 55, 1960, p. 1-8 ; Thomas D. Paxson Jr., « The Holism of Anaxagoras », ''Apeiron'', vol. 17, 1983, p. 85-91, pour qui seul le plénum existe par soi ; Adam Drozdek, « Anaxagoras and the Everything in Everything Principle », ''Hermes'', vol. 133, 2005, p. 163-177.</ref>.
=== Pas de plus petit ni de plus grand ===
Le troisième principe est exposé dans le fragment B3 : « Car du petit il n'y a pas de minimum, mais toujours un plus petit (car ce qui est ne peut pas ne pas être) ; mais du grand aussi il y a toujours un plus grand, et il est égal au petit en quantité ; et, rapportée à elle-même, chaque chose est à la fois grande et petite » (οὔτε γὰρ τοῦ σμικροῦ ἐστί τό γε ἐλάχιστον, ἀλλ' ἔλασσον ἀεί (τὸ γὰρ ἐὸν οὐκ ἔστι τὸ μὴ οὐκ εἶναι), ἀλλὰ καὶ τοῦ μεγάλου ἀεί ἐστι μεῖζον. καὶ ἴσον ἐστὶ τῷ σμικρῷ πλῆθος, πρὸς ἑαυτὸ δὲ ἕκαστόν ἐστι καὶ μέγα καὶ σμικρόν)<ref>Anaxagore, fragment B3, cité par Simplicius, ''Commentaire sur la Physique'', 164, 17-20 (DK 59 B 3). Zeller a proposé de corriger τὸ μή des manuscrits en τομῇ (« par division ») : la parenthèse signifierait alors que ce qui est ne peut cesser d'être par division. Burnet et Sider adoptent la correction ; Curd défend le texte des manuscrits (Curd, 2007, p. 39-40 ; Schofield, 1980, p. 156-157, n. 15).</ref>. L'absence de minimum dans cette théorie est incompatible avec l'atomisme de Leucippe et de Démocrite, qui pose des corps insécables<ref>Leucippe et Démocrite, DK 67-68 ; Aristote, ''Génération et corruption'', I, 2, 315b28-317a2, et I, 8, 325a23-b5.</ref>.
Contrairement à Leucippe et à Démocrite, qui posent des corps insécables (ἄτομα), Anaxagore soutient qu'aucune portion n'est minimale et que toute portion, si petite soit-elle, contient encore toutes choses<ref>Anaxagore, B6 : « Puisque les parts du grand et du petit sont égales en quantité, de cette manière aussi toutes choses seraient en tout » (καὶ ὅτε δὲ ἴσαι μοῖραί εἰσι τοῦ τε μεγάλου καὶ τοῦ σμικροῦ πλῆθος, καὶ οὕτως ἂν εἴη ἐν παντὶ πάντα).</ref>. Cette infinité dans la petitesse se double d'une infinité dans la grandeur : il n'existe pas plus de limite supérieure que de limite inférieure. Cette double infinité pose des problèmes interprétatifs importants, tant aux commentateurs anciens qu'aux modernes<ref>Montgomery Furth, « A “Philosophical Hero”? Anaxagoras and the Eleatics », ''Oxford Studies in Ancient Philosophy'', vol. 9, 1991, p. 95-129 ; David J. Furley, « Anaxagoras, Plato and the Naming of Parts », dans Victor Caston et Daniel W. Graham (éd.), ''Presocratic Philosophy: Essays in Honour of Alexander Mourelatos'', Aldershot, Ashgate, 2002, p. 119-126 ; Marmodoro, 2017, chap. 2-3.</ref>. Miloš Arsenijević, Saša Popović et Miloš Vuletić voient en lui un infinitiste conséquent, qui étend à la physique le principe d'isotropie de l'espace géométrique, si bien que deux parties quelconques du mélange originel sont semblables entre elles<ref>Miloš Arsenijević, Saša Popović et Miloš Vuletić, « Anaxagoras, the Thoroughgoing Infinitist: The Relation between his Teachings on Multitude and on Heterogeneity », ''European Journal of Analytic Philosophy'', vol. 15, 2019, p. 35-70, ici p. 35.</ref>.
Le principe de non-minimum a une conséquence importante : le mélange universel ne pourra jamais être défait. Puisqu'il n'existe pas de plus petite quantité d'un ingrédient, celui-ci ne peut jamais être entièrement extrait d'un mélange ; sa proportion peut diminuer indéfiniment sans jamais devenir nulle<ref>Anaxagore, B6 ; Curd, 2007, p. 182-185. Simplicius commente B3 en ces termes : si tout est en tout et si tout se sépare de tout, alors, de ce qui semble le plus petit, se séparera encore quelque chose de plus petit, et ce qui semble le plus grand s'est séparé de quelque chose de plus grand (''Commentaire sur la Physique'', 164, 20-23).</ref>. Comme le formule Malcolm Schofield à la suite de Colin Strang, la complexité de la composition n'est pas fonction de la taille<ref>Schofield, 1980, p. 69, 79 et 90 ; Strang, 1963, repris dans David J. Furley et R. E. Allen (éd.), ''Studies in Presocratic Philosophy'', vol. II, Londres, Routledge and Kegan Paul, 1975, p. 361-380, ici p. 366 ; cf. Curd, 2007, p. 40 et 50, n. 35.</ref>.
La justification donnée en B3 reprend l'axiome parménidien : « car ce qui est ne peut pas ne pas être ». Si l'on pouvait diviser un ingrédient jusqu'à le faire disparaître, il y aurait passage de l'être au non-être, ce qu'interdit Parménide. Toute division, aussi poussée soit-elle, laisse subsister quelque chose, et ce reste contient encore des parts de tous les ingrédients<ref>Curd, 2007, p. 39-40 et 184-185.</ref>.
=== Le principe de prédominance ===
Du principe « tout est dans tout » découle une difficulté : si chaque chose contient une part de toutes les autres, comment expliquer que nous percevions des objets distincts ? Anaxagore la résout par ce que les commentateurs modernes appellent le « principe de prédominance », expression qui n'est pas la sienne<ref>Schofield, 1980, p. 87 (où il cite les « principles of latency and predominance » de David Furley) et p. 108-111 ; Curd, 2007, p. 188-189 ; Marmodoro (2017, chap. 2) parle de ''preponderance principle''. Les commentateurs néoplatoniciens formulaient déjà les principes du « tout en tout » et de la prédominance (Schofield, 1980, p. 155, n. 52).</ref>. La fin du fragment B12 l'énonce ainsi : « Chaque chose singulière est et était, de la manière la plus manifeste, ce dont elle contient le plus » (ἀλλ' ὅτῳ πλεῖστα ἔνι, ταῦτα ἐνδηλότατα ἓν ἕκαστόν ἐστι καὶ ἦν)<ref>Anaxagore, fragment B12, fin, cité par Simplicius, ''Commentaire sur la Physique'', 156, 13-157, 4 (DK 59 B 12 ; LM 25 D27). La proposition qui précède (νοῦς δὲ πᾶς ὅμοιός ἐστι καὶ ὁ μείζων καὶ ὁ ἐλάττων· ἕτερον δὲ οὐδέν ἐστιν ὅμοιον οὐδενί) est diversement comprise ; A. Wasserstein propose de donner à ὅμοιος le même sens, « homogène », dans ses deux emplois, de sorte que la phrase opposerait l'homogénéité du Noûs au reste des choses (« A Note on Fragment 12 of Anaxagoras », ''Classical Review'', n.s. vol. 10, 1960, p. 4-5).</ref>.
Une chose tire donc son identité apparente des ingrédients qui y prédominent : un morceau d'or nous apparaît comme de l'or parce que l'or y est présent dans une proportion supérieure à celle des autres ingrédients<ref>Théophraste, dans Simplicius, ''Commentaire sur la Physique'', 27, 2-11 (DK 59 A 41) : chaque chose est caractérisée par ce qui prédomine en elle ; Aristote, ''Physique'', I, 4, 187b1-7.</ref>. Cette prédominance n'est jamais absolue, puisque tous les autres ingrédients demeurent présents, mais elle suffit à conférer à l'objet ses caractères perceptibles. Curd propose de la comprendre en termes de concentration ou de densité relative plutôt que de quantité brute<ref>Curd, 2007, p. 188-189.</ref>.
Le principe de prédominance permet de concilier le mélange universel avec l'expérience d'objets perceptiblement distincts. Dans l'explication doxographique de la nutrition, les parties de chair, d'os et d'autres tissus qui nourrissent le corps sont supposées être déjà présentes, de manière imperceptible, dans la nourriture<ref>Aétius, I, 3, 5 (DK 59 A 46) ; Simplicius, ''Commentaire sur la Physique'', 460, 4-20 (DK 59 A 45) ; Platon, ''Phédon'', 96c-d (DK 59 A 46).</ref>. Le changement apparent se ramène ainsi à la recomposition de constituants préexistants et à des variations de prédominance<ref>Curd, 2007, p. 188-191.</ref>.
Selon Théophraste, Anaxagore soutenait que la perception se fait par les contraires et que toute sensation s'accompagne de douleur, parce que le contact du dissemblable produit une gêne<ref>Théophraste, ''Du sens'', 27-29 (DK 59 A 92) ; Aétius, IV, 9, 16 (DK 59 A 94).</ref>. Les sens ne discernent que les différences marquées de proportion ; Anaxagore reconnaissait leur faiblesse, tout en affirmant que « les choses qui apparaissent sont une vue des choses invisibles » (ὄψις γὰρ τῶν ἀδήλων τὰ φαινόμενα)<ref>Anaxagore, fragments B21 (Sextus Empiricus, ''Contre les mathématiciens'', VII, 90 : « à cause de la faiblesse des sens, nous ne sommes pas capables de discerner le vrai ») et B21a (Sextus Empiricus, ''Contre les mathématiciens'', VII, 140) ; Schofield, 1980, p. 24-25.</ref>.
=== Synthèse : la réponse d'Anaxagore à Parménide ===
Les quatre principes examinés, à savoir le remplacement de la génération et de la destruction par le mélange et la dissociation, le « tout est dans tout », l'absence de plus petit et la prédominance, forment un ensemble cohérent qui constitue la réponse d'Anaxagore à l'exigence parménidienne<ref>Curd, 2007, p. 137-142 et 178-191 ; Schofield, 1980, p. 36-99.</ref>. Anaxagore accepte l'impossibilité du passage de l'être au non-être, mais rejette, ou du moins ne reprend pas, le monisme numérique que la lecture traditionnelle attribue à Parménide, et il admet le mouvement.
En remplaçant la génération et la corruption par le mélange et la dissociation (B17), Anaxagore rend compte des phénomènes sans faire provenir une réalité nouvelle du non-être. Le « tout est dans tout » (B6, B11), l'absence de minimum (B3, B6) et la prédominance (B12) forment, dans les principales reconstructions modernes, un ensemble destiné à expliquer la persistance des ingrédients et la diversité des apparences<ref>Curd, 2007, p. 137-142 et 178-191 ; Schofield, 1980, p. 36-99.</ref>.
Anaxagore entend ainsi préserver la permanence de ce qui est et l'impossibilité du non-être tout en rendant compte de la pluralité et du devenir. Les spécialistes restent partagés sur le degré de son engagement éléate : Curd met l'accent sur la dimension parménidienne du système, Schofield sur sa fidélité au style dogmatique de la cosmologie ionienne<ref>Curd, 2007, p. 141-142 ; Schofield, 1980, p. 26-28 et 142-143 ; Daniel W. Graham, ''Explaining the Cosmos'', 2006.</ref>.
== Les ingrédients primordiaux ==
La question de savoir quels sont exactement les ingrédients (τὰ χρήματα) qui composent l'univers d'Anaxagore divise les commentateurs depuis Aristote<ref>Pour une vue d'ensemble : Schofield, 1980, p. 100-144 ; Curd, 2007, p. 147-150 et 153-191 ; Daniel W. Graham, « Was Anaxagoras a Reductionist? », ''Ancient Philosophy'', vol. 24, 2004, p. 1-18 ; Curd et Sisko, ''SEP'', 2026, § 3.2.</ref>. Anaxagore ne fournit pas de liste systématique, et les fragments conservés mentionnent des entités de natures apparemment différentes. Schofield observe que, malgré le « rôle cardinal » de la doctrine du « tout est dans tout », les fragments montrent qu'Anaxagore accordait au moins autant de place, dans son exposé, au mélange primordial et à l'action cosmogonique du Noûs<ref>Schofield, 1980, p. 100.</ref>.
=== Les opposés ===
Les fragments B4b, B8, B12 et B15 énumèrent plusieurs couples d'opposés : l'humide et le sec (τὸ διερόν καὶ τὸ ξηρόν), le chaud et le froid (τὸ θερμόν καὶ τὸ ψυχρόν), le brillant et l'obscur (τὸ λαμπρόν καὶ τὸ ζοφερόν), le dense et le rare (τὸ πυκνόν καὶ τὸ ἀραιόν)<ref>Anaxagore, B4b, B8, B12, B15. Les fragments B1 et B2 mentionnent l'air et l'éther, non des couples d'opposés.</ref>. Ces opposés jouent un rôle cosmologique central : c'est leur séparation progressive (ἀποκρίνεσθαι) à partir du mélange originel qui produit la diversité des phénomènes<ref>Anaxagore, B12, B13, B15, B16 ; Aristote, ''Physique'', I, 4, 187a25-26 et 187b1-7.</ref>.
La distinction entre substance et qualité, et la notion technique de « matière » (ὕλη), sont des élaborations aristotéliciennes qu'il faut éviter de projeter sans précaution sur Anaxagore<ref>Curd, 2007, p. 141, n. 32. Burnet (1930, chap. VI) souligne que le chaud et le froid, le sec et l'humide sont pour Anaxagore des « choses » (χρήματα).</ref>. Dans les lectures qui traitent les opposés comme des ingrédients fondamentaux, le chaud, le froid, le sec ou l'humide ne sont donc pas de simples propriétés d'un substrat indéterminé. F. M. Cornford parle à leur propos de « choses-qualités » (''quality-things'')<ref>F. M. Cornford, « Anaxagoras' Theory of Matter », ''Classical Quarterly'', vol. 24, 1930, p. 14-30 et 83-95, ici p. 84 (cité par Teodorsson, 1982, p. 35).</ref> ; Gregory Vlastos les décrit comme des puissances dont la combinaison en certaines proportions produit les substances naturelles<ref>Vlastos, 1950, repris dans ''Studies in Greek Philosophy'', vol. I, éd. Daniel W. Graham, Princeton, Princeton University Press, 1995, p. 303-327, ici p. 322 (cité par Curd, 2007, p. 168, n. 31).</ref>.
=== L'interprétation « austère » : les opposés seuls ===
Paul Tannery fut l'un des premiers à contester l'interprétation aristotélicienne selon laquelle Anaxagore aurait posé des « homéomères » élémentaires (chair, os, etc.) et à faire des opposés les véritables constituants<ref>Paul Tannery, « La théorie de la matière d'Anaxagore », ''Revue philosophique'', vol. 22, 1886, p. 255-274 ; repris dans ''Pour l'histoire de la science hellène'', Paris, Alcan, 1887 (Burnet renvoie à la p. 283 sq.). Voir Teodorsson, 1982, p. 30.</ref>. John Burnet adopta une position voisine ; il remarquait que, même après la définition de la notion de qualité (ποιότης), cette manière de penser les opposés comme des choses avait survécu, et s'appuyait sur Galien, pour qui les qualités sont éternelles chez Anaxagore<ref>Burnet, 1930, chap. VI, section « The Portions », qui cite Galien, ''Des facultés naturelles'', I, 2.</ref>.
Les défenseurs de cette interprétation s'appuient notamment sur le rôle cosmologique explicite des opposés dans B12, B15 et B16 et sur le fait que le vocabulaire aristotélicien des homéomères n'apparaît pas dans les fragments<ref>Schofield, 1980, p. 107-121 ; Curd, 2007, p. 147-150, 156-157 et 163-166.</ref>. Des versions diverses de cette lecture ont été défendues par Tannery, Burnet, Cornford, Vlastos, Schofield, Inwood, Sedley et Marmodoro<ref>Curd, 2007, p. 156, n. 8, et p. 164, n. 21 ; Marmodoro, 2017, chap. 1, § 1.1, n. 4 ; Curd et Sisko, ''SEP'', 2026, § 3.2 ; David Sedley, ''Creationism and Its Critics in Antiquity'', Berkeley, University of California Press, 2007. Schofield (1980, p. 114-116 et 132-133) distingue des ingrédients fondamentaux (les opposés) et des ingrédients dérivés (air, terre, eau, semences) ; il a ensuite nuancé sa position (Curd, 2007, p. 156, n. 8). Marmodoro comprend les opposés comme des puissances et tient les substances pour réductibles aux opposés.</ref>.
=== L'interprétation « expansive » et le témoignage d'Aristote ===
La question devient plus complexe lorsqu'on prend en compte les témoignages indirects. Aristote attribue à Anaxagore une doctrine des « homéomères » (τὰ ὁμοιομερῆ), substances dont les parties portent le même nom que le tout, comme la chair, l'os ou la moelle<ref>Aristote, ''Génération et corruption'', I, 1, 314a18-20 (DK 59 A 46) ; ''Du ciel'', III, 3, 302a28-b4 (DK 59 A 43) ; ''Physique'', I, 4, 187a25-26.</ref>.
Le terme d'homéomère n'apparaît pourtant dans aucun fragment. Il appartient au vocabulaire d'Aristote, pour qui les homéomères constituent un niveau intermédiaire entre les éléments et les organes<ref>Aristote, ''Parties des animaux'', II, 1, 646a12-24 ; Curd, 2007, p. 147-150. Burnet (1930, chap. VI) jugeait étrange, si Anaxagore avait employé le terme, que Simplicius ne cite aucun fragment qui le contienne.</ref>. Curd montre en outre qu'Aristote ne prête pas à Anaxagore un « principe d'homéomérie » au sens strict, et qu'il se trompe en faisant de l'air et du feu des mélanges d'homéomères<ref>Curd, 2007, p. 148-150.</ref>. D'autres interprètes réhabilitent au contraire la notion : William E. Mann en fait le centre de son interprétation, et John Sisko juge l'homéomérie compatible avec les autres principes d'Anaxagore et probablement authentique<ref>William E. Mann, « Anaxagoras and the ''Homoiomerē'' », ''Phronesis'', vol. 25, 1980, p. 228-249, ici p. 228 et 231-233 ; John E. Sisko, « Anaxagoras on Matter, Motion, and Multiple Worlds », ''Philosophy Compass'', vol. 5, 2010, p. 443-454, ici p. 443.</ref>.
Une lecture « expansive » soutient néanmoins que tout ce qui apparaît dans le monde sensible se trouvait déjà dans le mélange originel. Elle a été défendue, sous des formes diverses, par Strang, Stokes, Guthrie, Barnes, Furth et Graham ; Peck en proposait une version qui écartait les substances inorganiques<ref>Curd, 2007, p. 154-156 et n. 3-4, p. 158-159, n. 12 ; Curd et Sisko, ''SEP'', 2026, § 3.2 ; Arthur L. Peck, « Anaxagoras: Predication as a Problem in Physics », ''Classical Quarterly'', vol. 25, 1931, p. 27-37 et 112-120, ici p. 30, où les substances organiques sont tenues pour élémentaires et les substances inorganiques pour dérivées. Kerferd (1969) admet une forme d'homéomérie compatible avec le mélange universel (Curd et Sisko, § 3.7).</ref>. Gershenson et Greenberg proposent de leur côté une lecture particulaire dans laquelle les tissus organiques, conçus comme des « molécules » infinitésimales, sont les éléments de toute matière<ref>Daniel E. Gershenson et Daniel A. Greenberg, ''Anaxagoras and the Birth of Scientific Method'', New York, Blaisdell, 1964, p. 9-12 et 14-22.</ref>.
=== L'interprétation modérée ===
Une troisième voie a été proposée par Patricia Curd, dont la position est, selon elle, proche de celle de William E. Mann, sauf pour les semences, et a été reprise par John Sisko<ref>Curd, 2007, p. 157-171 et n. 10 ; Mann, 1980 ; Curd et Sisko, ''SEP'', 2026, § 3.2.</ref>. Selon cette lecture, les ingrédients comprennent les opposés et diverses substances : terre, air, éther, eau, feu, métaux, chair, sang, os. En revanche, les plantes, les animaux et leurs organes ne sont pas des ingrédients primordiaux, mais des composés temporaires, des « artefacts naturels » selon l'expression de Curd<ref>Curd, 2007, p. 157-163 et 170-171.</ref>.
Cette lecture tient compte à la fois des fragments et des témoignages d'Aristote. Elle reconnaît le rôle cosmologique des opposés (B12, B15) tout en admettant que d'autres substances figurent au même niveau dans le mélange : l'air et l'éther recouvrent tout avant même la rotation (B1), et B4b mentionne la terre et les semences à côté des opposés<ref>Curd, 2007, p. 166-167.</ref>.
=== Les semences (σπέρματα) ===
Les « semences » (σπέρματα), mentionnées dans les fragments B4a et B4b, ajoutent une difficulté supplémentaire. Selon B4b, avant toute séparation, aucune couleur n'était manifeste, car le mélange de toutes choses l'empêchait, « la terre étant présente en abondance et des semences illimitées en nombre, en rien semblables les unes aux autres » (καὶ γῆς πολλῆς ἐνεούσης καὶ σπερμάτων ἀπείρων πλῆθος οὐδὲν ἐοικότων ἀλλήλοις)<ref>Anaxagore, fragment B4b, cité par Simplicius, ''Commentaire sur la Physique'', 34, 21-26 (DK 59 B 4b). Sur la division de DK B4 en B4a et B4b, voir Curd, 2007, p. 42.</ref>. Le fragment B4a déclare : « Puisqu'il en est ainsi, il faut penser qu'il y a beaucoup de choses de toutes sortes dans toutes les choses qui se composent, et des semences de toutes choses, ayant des formes, des couleurs et des saveurs de toute espèce »<ref>Anaxagore, fragment B4a, cité par Simplicius, ''Commentaire sur la Physique'', 34, 29-35, 9 (DK 59 B 4a). Simplicius précise que le passage se trouvait peu après le début du livre.</ref>.
La nature de ces semences est débattue. Gregory Vlastos voyait dans σπέρμα un terme technique désignant un agrégat de tous les ingrédients dans lequel l'un d'eux prédomine ; G. E. R. Lloyd et W. K. C. Guthrie ont adopté des positions voisines<ref>Vlastos, 1950, repris dans Furley et Allen (éd.), 1975, vol. II, p. 323-353, ici p. 324 ; G. E. R. Lloyd, ''Polarity and Analogy'', Cambridge, Cambridge University Press, 1966, p. 246-247 ; Guthrie, 1965, p. 298-300. Voir Schofield, 1980, p. 123 et n. 38-39.</ref>. Une lecture aujourd'hui répandue, défendue par David Furley, Malcolm Schofield, Patricia Curd, David Sedley et Anna Marmodoro, comprend les semences au sens biologique ordinaire, comme les germes à partir desquels croissent plantes et animaux<ref>David J. Furley, « Anaxagoras in Response to Parmenides », ''Canadian Journal of Philosophy'', suppl. vol. 2, 1976, p. 61-85 ; Schofield, 1980, p. 123-126 ; Curd, 2007, p. 171-177 ; Sedley, 2007 ; Marmodoro, 2017, chap. 5.</ref>. Marmodoro précise ce rôle : les semences sont des « cadres » physiques auxquels s'ajoutent les opposés dispersés par le tourbillon, et elles possèdent une efficacité causale propre, celle de « puissances de vie » qui règlent le développement structurel des organismes<ref>Marmodoro, 2017, p. 7 et 147-153.</ref>. D'autres en font des « homoncules » préformés de tous les organismes à venir (Eric Lewis), ou encore des « points de croissance » dont le développement serait réglé d'avance par le Noûs (Teodorsson)<ref>Eric Lewis, « Anaxagoras and the Seeds of a Physical Theory », ''Apeiron'', vol. 33, 2000, p. 1-23, ici p. 1 et 16-19 ; Teodorsson, 1982, p. 80-91, qui recense aussi les interprétations antérieures des semences (p. 45 et suiv.). David Sider juge l'hypothèse de Teodorsson ingénieuse mais insuffisamment argumentée (compte rendu de Teodorsson, 1982, ''Classical Journal'', vol. 80, 1984, p. 71-73, ici p. 72). Critique de la lecture en homoncules chez Mann, 1980, p. 236, et Curd, 2007, p. 172-175.</ref>. Aristote, pour sa part, semble avoir rangé sous ce terme les homéomères eux-mêmes<ref>Aristote, ''Du ciel'', III, 3, 302a28-b4 (DK 59 A 43) ; ''Génération et corruption'', I, 1, 314a28-b1 ; Curd, 2007, p. 151-152 ; Schofield, 1980, p. 128-132.</ref>.
Schofield et Curd défendent la lecture biologique de σπέρματα en s'appuyant sur l'usage ordinaire du mot et sur le contexte de B4a, où figurent des hommes, des animaux et des cultures<ref>Schofield, 1980, p. 123-126 ; Curd, 2007, p. 171-177.</ref>. Cette lecture est également rapprochée des témoignages selon lesquels l'air transporte des semences qui, entraînées avec la pluie, produisent les plantes<ref>Théophraste, ''Recherches sur les plantes'', III, 1, 4 (DK 59 A 117 ; cf. ''Causes des plantes'', I, 5, 2) ; Irénée, ''Contre les hérésies'', II, 14, 2 (DK 59 A 113).</ref>. Diogène Laërce rapporte par ailleurs que les animaux naquirent d'abord de l'humide, du chaud et du terreux, puis les uns des autres<ref>Diogène Laërce, II, 9 (DK 59 A 1) ; cf. Hippolyte, ''Réfutation de toutes les hérésies'', I, 8, 12 (DK 59 A 42).</ref>.
== L'état originel : tout ensemble ==
Le traité d'Anaxagore s'ouvrait sur l'une des déclarations les plus célèbres de la philosophie présocratique : « Toutes choses étaient ensemble » (ὁμοῦ χρήματα πάντα ἦν)<ref>Anaxagore, fragment B1, cité par Simplicius, ''Commentaire sur la Physique'', 155, 26-30 (DK 59 B 1 ; LM 25 D9). Diels et Kranz impriment ὁμοῦ πάντα χρήματα ἦν ; Wolfgang Rösler a montré que les deux citations les plus complètes de Simplicius (''Commentaire sur la Physique'', 155, 26-30 ; ''Commentaire sur le Ciel'', 608, 21-23) portent ὁμοῦ χρήματα πάντα ἦν, ordre retenu par Sider et par Curd (Rösler, « ΟΜΟΥ ΧΡΗΜΑΤΑ ΠΑΝΤΑ ΗΝ », ''Hermes'', vol. 99, 1971, p. 246-248 ; Curd, 2007, p. 33 ; Schofield, 1980, p. 151, n. 1). La phrase citée par Diogène Laërce (II, 6), « Toutes choses étaient ensemble ; puis l'Intellect vint et les ordonna », est un résumé et non un fragment (Burnet, 1930, chap. VI).</ref>. Cette formule d'ouverture exprime la thèse cosmogonique d'Anaxagore sur l'état primordial de l'univers ; Schofield souligne que le livre commençait sans préambule personnel, en exposant d'emblée ce thème<ref>Schofield, 1980, p. 36-40.</ref>.
Owen et Schofield lisent la formule d'ouverture d'Anaxagore comme une réponse délibérée à Parménide<ref>Owen, 1966, repris dans Mourelatos (éd.), 1974, p. 276-277 ; Schofield, 1980, p. 64.</ref>. Là où Parménide affirmait que l'être « est maintenant tout entier ensemble, un, continu » (νῦν ἔστιν ὁμοῦ πᾶν, ἕν, συνεχές)<ref>Parménide, B 8, 5-6 (DK 28 B 8).</ref>, Anaxagore ouvre son traité par « toutes choses étaient ensemble ». Dans cette interprétation, le pluriel et le temps passé de la formule marquent une prise de distance à l'égard de certains traits de l'être parménidien<ref>Schofield, 1980, p. 64-65 ; Curd, 2007, p. 153-154.</ref>.
=== La description du mélange originel ===
Le fragment B1, que Simplicius situe au début du premier livre, décrit cet état primordial<ref>Simplicius, ''Commentaire sur la Physique'', 155, 26-30 (DK 59 B 1) ; Curd, 2007, p. 33-36.</ref> :
<blockquote>Toutes choses étaient ensemble, illimitées en quantité et en petitesse, car le petit aussi était illimité. Et toutes choses étant ensemble, rien n'était manifeste en raison de la petitesse ; car l'air et l'éther recouvraient toutes choses, étant l'un et l'autre illimités : ce sont eux, en effet, les plus grands dans l'ensemble des choses, et en quantité et en grandeur.<ref>Anaxagore, fragment B1 (DK 59 B 1). Le verbe κατεῖχεν est rendu ici par « recouvraient » ; Burnet le traduisait par « prédominaient », et David Sider a défendu cette lecture : comme toute chose se caractérise par ce qui prédomine en elle (B12), le mélange originel aurait présenté l'aspect de l'air et de l'éther, qui y prédominaient (« A Note on Anaxagoras, Fr. 1 », ''Archiv für Geschichte der Philosophie'', vol. 55, 1973, p. 249-251). Gigon jugeait cette traduction difficile sur le plan lexical.</ref></blockquote>
B1 énonce ainsi que toutes choses étaient ensemble, qu'elles étaient illimitées en quantité et en petitesse, que rien n'était manifeste en raison de la petitesse et que l'air et l'éther recouvraient toutes choses<ref>Anaxagore, B1 (DK 59 B 1) ; Curd, 2007, p. 33-36.</ref>.
L'expression « illimitées en quantité et en petitesse » a donné lieu à deux lectures principales, que Schofield a nommées interprétation « particulaire » et interprétation « proportionnelle »<ref>Schofield, 1980, p. 70-79.</ref>. La première comprend que le mélange originel contenait une infinité de petites particules distinctes<ref>Vlastos, 1950 ; Guthrie, 1965, p. 289 ; Kerferd, 1969 ; Sider, 1981 (voir Curd, 2007, p. 183, n. 9 ; Curd et Sisko, ''SEP'', 2026, § 3.3).</ref>. La seconde refuse de concevoir le mélange comme une collection de particules : chaque ingrédient y est présent en une proportion aussi faible qu'on voudra par rapport à l'ensemble<ref>Schofield, 1980, p. 73-79 ; Barnes, 1982, p. 323-326 ; Inwood, 1986, p. 17-18. Curd (2007, p. 181-187) propose un modèle de « densités » : les ingrédients, comparables à des liquides ou à des pâtes, sont présents partout à des concentrations variables.</ref>. Le débat n'est pas tranché<ref>Pour une discussion d'ensemble, voir Curd, 2007, p. 181-191, et Marmodoro, 2017, chap. 4 (p. 105-127).</ref>.
=== L'imperceptibilité du mélange ===
B1 décrit le mélange originel comme non manifeste (οὐδὲν ἔνδηλον ἦν) et relie cette non-manifestation à la petitesse ainsi qu'au fait que l'air et l'éther « recouvraient toutes choses »<ref>Anaxagore, B1 (DK 59 B 1).</ref>.
L'air (ἀήρ) désigne ici, selon l'usage ionien ancien, une brume sombre, humide, froide et dense ; l'éther (αἰθήρ), la substance brillante, chaude, sèche et rare, qu'Aristote dit identifiée au feu par Anaxagore<ref>Théophraste, ''Du sens'', 59 (DK 59 A 70) : le rare et le fin sont chauds, le dense et l'épais froids, comme Anaxagore définit l'éther et l'air ; Aristote, ''Du ciel'', I, 3, 270b24-25 (DK 59 A 73), et III, 3, 302b4 ; Schofield, 1980, p. 71. Peter Kingsley souligne qu'il s'agit d'une interprétation d'Aristote, plus prudent dans les ''Météorologiques'' : chez Anaxagore, l'éther serait plutôt un air sec qui tend vers le haut, dont le feu dérive comme l'eau et la terre dérivent de l'air humide (« Notes on Air: Four Questions of Meaning in Empedocles and Anaxagoras », ''Classical Quarterly'', n.s. vol. 45, 1995, p. 26-29, ici p. 28-29).</ref>. B1 dit seulement que l'air et l'éther sont les plus grands dans l'ensemble des choses, en quantité et en grandeur. Sider a proposé d'interpréter le verbe κατέχειν comme indiquant leur prédominance dans le mélange originel, lecture qui reste discutée<ref>David Sider, « A Note on Anaxagoras, Fr. 1 », ''Archiv für Geschichte der Philosophie'', vol. 55, 1973, p. 249-251 ; Schofield, 1980, p. 155-156, n. 5-6 ; Curd, 2007, p. 178 et n. 1.</ref>.
Le fragment B4b fournit une autre description de l'indistinction initiale<ref>Anaxagore, B4b (DK 59 B 4b) ; Curd, 2007, p. 45-47.</ref> :
<blockquote>Mais avant que ces choses ne fussent séparées, toutes étant ensemble, aucune couleur n'était manifeste ; car le mélange de toutes choses l'empêchait, celui de l'humide et du sec, du chaud et du froid, du brillant et de l'obscur, la terre étant présente en abondance et des semences illimitées en nombre, en rien semblables les unes aux autres.<ref>Anaxagore, fragment B4b (DK 59 B 4b).</ref></blockquote>
L'indistinction du mélange originel ne tenait donc pas à l'absence des ingrédients, mais à leur mélange si intime qu'aucun ne pouvait se manifester. Curd souligne qu'il s'agit d'un contrefactuel : aucun observateur n'était présent, mais un observateur n'aurait rien pu y distinguer<ref>Curd, 2007, p. 46.</ref>.
=== L'immobilité originelle ===
Plusieurs témoignages rapportent qu'avant l'intervention du Noûs, le mélange était au repos depuis un temps illimité<ref>Aristote, ''Physique'', VIII, 1, 250b24-26 ; Simplicius, ''Commentaire sur la Physique'', 1121, 21 (DK 59 A 64) ; Aétius, I, 7, 5 (DK 59 A 48).</ref>. Cette immobilité pose une difficulté : si le mélange était au repos, qu'est-ce qui a pu le mettre en mouvement ? Anaxagore répond en posant le Noûs (Νοῦς, Intellect), distinct de tous les ingrédients, qui possède le pouvoir d'initier le mouvement<ref>Anaxagore, B12 et B13.</ref>. Eudème reprochait déjà à Anaxagore de faire commencer le mouvement à un moment donné sans dire s'il cesserait un jour<ref>Simplicius, ''Commentaire sur la Physique'', 1185, 9 (DK 59 A 59). Une colonne d'un papyrus d'Herculanum (Philodème) attribue au contraire à Anaxagore l'idée d'un mouvement éternel ; voir Christian Vassallo, ''The Presocratics at Herculaneum'', Berlin et Boston, De Gruyter, 2021, et Curd et Sisko, ''SEP'', 2026, § 4.4.</ref>.
=== L'étendue du mélange originel ===
Le fragment B1 affirme que l'air et l'éther « étaient l'un et l'autre illimités » (ἀμφότερα ἄπειρα ἐόντα), et le fragment B2 précise que « l'air et l'éther se séparent de la masse environnante, et la masse environnante est illimitée en quantité »<ref>Anaxagore, fragment B2 (DK 59 B 2).</ref>. Ces fragments ont généralement été compris comme impliquant que le mélange originel était spatialement illimité, le sens exact d'ἄπειρον (« illimité » ou « indéfini ») restant discuté<ref>Aristote, ''Physique'', III, 4, 203a19-33 (DK 59 A 45) ; III, 5, 205b1-5 (DK 59 A 50), où Aristote critique l'idée que l'illimité se fixe lui-même en place ; sur les sens d'ἄπειρον, voir Curd et Sisko, ''SEP'', 2026, § 3.2 et 3.5.</ref>. Le fragment B12 indique que la révolution a commencé à partir d'une petite région, qu'elle s'étend maintenant davantage et qu'elle s'étendra davantage encore<ref>Anaxagore, B12 (DK 59 B 12).</ref>. Selon Curd, la description de B1 continue ainsi de valoir pour les régions du mélange que l'expansion de la rotation n'a pas encore atteintes<ref>Curd, 2007, p. 207-208.</ref>.
== Le Noûs : l'Intellect cosmique ==
Le fragment B12, le plus long des fragments conservés, est presque entièrement consacré au Noûs (Νοῦς, « Intellect » ou « Esprit »). Schofield y voit l'un des passages les plus puissants de toute la prose grecque, par son intensité et sa lente grandeur<ref>Schofield, 1980, p. 4 ; sur le style de « prédication solennelle », voir Deichgräber, 1933, et Schofield, 1980, p. 6-9.</ref>. Anaxagore y expose la nature du Noûs et son rôle dans la formation du monde. La tradition a souvent tenu cette doctrine pour son apport le plus original ; Diogène Laërce et Plutarque rapportent qu'on le surnommait lui-même « Noûs », surnom que Timon tourne en dérision dans ses ''Silles''<ref>Diogène Laërce, II, 6 (DK 59 A 1), qui cite Timon ; Plutarque, ''Vie de Périclès'', 4 (DK 59 A 15) ; Curd, 2007, p. 192. Burnet (1930, chap. VI) jugeait au contraire que l'originalité d'Anaxagore tenait davantage à sa théorie de la substance qu'à celle du Noûs. Walter Bröcker constatait ce partage : Zeller et Capelle voyaient l'apport d'Anaxagore dans sa doctrine de l'intellect, Tannery et Burnet dans sa théorie de la matière (« Die Lehre des Anaxagoras », ''Kant-Studien'', vol. 42, 1942-1943, p. 176-189, ici p. 176).</ref>.
Deux questions doivent être distinguées. B12 attribue explicitement au Noûs le contrôle de la révolution, le « discernement » (γνώμη) et une relation exprimée par ἔγνω avec les choses mêlées, séparées et dissociées. Curd et Lesher lisent ces termes comme l'attribution d'une connaissance et d'un discernement véritables ; d'autres interprètes, notamment Sider dans une lecture en termes de loi naturelle, réduisent la portée psychologique de ce vocabulaire<ref>Anaxagore, B12 ; James H. Lesher, « Mind's Knowledge and Powers of Control in Anaxagoras DK B12 », ''Phronesis'', vol. 40, 1995, p. 125-142 ; Curd, 2007, p. 193-205 ; Curd et Sisko, ''SEP'', 2026, § 4.1-4.2.</ref>. Aucun fragment conservé n'affirme en revanche que le Noûs dispose les choses de la manière qui est la meilleure ; c'est ce que le Socrate du ''Phédon'' dit avoir espéré trouver chez Anaxagore<ref>Platon, ''Phédon'', 97b-98c (DK 59 A 47) ; Curd, 2007, p. 144-145 et 204-205.</ref>.
Curd met en garde contre l'application rétrospective aux présocratiques de la classification aristotélicienne des quatre causes et qualifie elle-même d'anachronique l'expression de « cause efficiente » lorsqu'elle est appliquée sans précaution à Anaxagore<ref>Patricia Curd, « Presocratic Philosophy », ''Stanford Encyclopedia of Philosophy'', 2007, révision substantielle 2020, § 2 ; Curd, 2007, p. 141, n. 32, et p. 198.</ref>. La question de la finalité du Noûs doit donc être distinguée de la terminologie causale élaborée plus tard par Aristote.
=== La séparation du Noûs d'avec toutes choses ===
Le fragment B12 s'ouvre par l'affirmation suivante sur le Noûs<ref>Anaxagore, B12 (DK 59 B 12) ; Curd, 2007, p. 57-66.</ref> :
<blockquote>Les autres choses ont part à toute chose, mais l'Intellect est illimité et maître de lui-même, il n'est mêlé à aucune chose, mais il est seul, lui-même par lui-même.<ref>Anaxagore, fragment B12, cité par Simplicius, ''Commentaire sur la Physique'', 156, 13-15 (DK 59 B 12). Simplicius cite la première proposition en 164, 24-25.</ref></blockquote>
Le Noûs fait ainsi exception au principe du mélange universel : alors que tous les ingrédients contiennent des parts de tous les autres, lui seul demeure pur et séparé<ref>Anaxagore, B11 (DK 59 B 11).</ref>. Le fragment B14 affirme pourtant qu'il est là où sont toutes les autres choses : dans la masse environnante, dans ce qui s'est adjoint et dans ce qui s'est séparé<ref>Anaxagore, B14, cité par Simplicius, ''Commentaire sur la Physique'', 157, 5-7 (DK 59 B 14). Le début du fragment est altéré ; voir David Sider, « Anaxagoras Fr. 14 DK », ''Hermes'', vol. 102, 1974, p. 365-367.</ref>. Trois attributs lui sont d'abord prédiqués : il est « illimité » (ἄπειρον), « maître de lui-même » (αὐτοκρατές) et « mêlé à aucune chose ». Anaxagore justifie cette séparation par un argument :
<blockquote>Car s'il n'était pas par lui-même, mais s'il était mêlé à quelque autre chose, il aurait part à toutes choses, s'il était mêlé à l'une d'elles ; car en toute chose il y a une part de toute chose, comme je l'ai dit auparavant. Et les choses mêlées à lui l'empêcheraient, de sorte qu'il ne dominerait aucune chose comme il le fait, étant seul par lui-même.<ref>Anaxagore, B12, cité par Simplicius, ''Commentaire sur la Physique'', 156, 15-20.</ref></blockquote>
Il s'agit d'une preuve indirecte : si le Noûs était mêlé à quoi que ce soit, il serait mêlé à tout ; les choses mêlées à lui l'empêcheraient d'exercer son pouvoir ; or il l'exerce ; il n'est donc mêlé à rien<ref>Schofield, 1980, p. 7 et 19, qui voit dans l'usage de cette forme d'argument un possible indice d'influence éléate (p. 146, n. 15).</ref>. Anaxagore n'explique pas pourquoi le mélange entraverait l'action du Noûs, et plusieurs explications ont été proposées<ref>Curd, 2007, p. 58-59 et 200-201 ; Schofield, 1980, p. 147-148, n. 39.</ref>.
=== Les attributs du Noûs ===
Après avoir établi la séparation du Noûs, Anaxagore énonce une série d'attributs dans le style de la prédication solennelle, qu'Eduard Norden et Karl Deichgräber ont rapproché de l'hymne religieux<ref>Deichgräber, 1933, p. 347-361 ; Eduard Norden, ''Agnostos Theos'', Leipzig, Teubner, 1913 (4{{e}} tirage, Stuttgart, 1956), p. 143-176 ; Schofield, 1980, p. 6-9 et 12.</ref> :
<blockquote>Car il est la plus fine de toutes les choses et la plus pure ; il détient tout discernement (γνώμη) sur toute chose et il a la plus grande force ; et toutes les choses qui ont une âme, les plus grandes comme les plus petites, l'Intellect les domine.<ref>Anaxagore, B12, cité par Simplicius, ''Commentaire sur la Physique'', 156, 20-24.</ref></blockquote>
Le premier attribut, qui qualifie le Noûs de « plus fin » (λεπτότατον) et « plus pur » (καθαρώτατον), a reçu des interprétations divergentes, que Schofield ramène à trois<ref>Schofield, 1980, p. 11-12.</ref>. Pour les uns, il s'agit d'une substance matérielle d'une extrême finesse : Anaxagore emploie probablement λεπτός en un sens physique à propos de l'eau de mer, et il parle d'un intellect « plus grand » ou « plus petit »<ref>Aétius, III, 16, 2 (DK 59 A 90) ; Burnet, 1930, p. 268 ; J. E. Raven, « The Basis of Anaxagoras' Cosmology », ''Classical Quarterly'', n.s. vol. 4, 1954, p. 123-137, ici p. 134-135 ; Barnes, 1982, p. 406-409 ; Sider, 1981. Voir Schofield, 1980, p. 147, n. 28, et Curd, 2007, p. 59, n. 50.</ref>. Pour d'autres, ces termes visent à exprimer l'incorporéité du Noûs<ref>Guthrie, 1965, p. 276-278 ; Curd, 2007, p. 59 et 200 ; Gershenson et Greenberg, 1964, p. 32-33, qui font d'Anaxagore le premier penseur grec à introduire une entité substantielle incorporelle.</ref>. Une troisième lecture, celle d'Aristote, à laquelle Schofield se rallie, comprend que le Noûs n'a aucun caractère propre qui le rendrait semblable aux choses qu'il connaît et domine<ref>Aristote, ''De l'âme'', III, 4, 429a18-24 (DK 59 A 100) ; Schofield, 1980, p. 11-12.</ref>. On a longtemps soutenu que la notion d'incorporéité n'était pas encore disponible à l'époque d'Anaxagore ; Zeller jugeait qu'il avait voulu parler d'un incorporel sans y parvenir<ref>Burnet, 1930, chap. VI, qui rapporte la position de Zeller ; Raven, 1954, p. 130, pour qui aucun présocratique n'avait encore saisi l'existence de l'incorporel.</ref>. Curd rappelle toutefois que Mélissos refuse déjà un corps à l'Un<ref>Mélissos, DK 30 B 9 ; Curd, 2007, p. 59.</ref>.
Le deuxième attribut concerne la connaissance : le Noûs « détient tout discernement sur toute chose ». Le troisième concerne la puissance : il « a la plus grande force ». Anaxagore suggère le lien entre les deux par une assonance, ἴσχει (« il détient ») et ἰσχύει (« il a de la force »)<ref>Schofield, 1980, p. 15 et p. 147, n. 36 ; Curd, 2007, p. 60. James Lesher souligne que γνώμη unit connaissance et détermination : le Noûs juge et décide de toutes choses, mais à partir d'un savoir (« Mind's Knowledge and Powers of Control in Anaxagoras DK B12 », ''Phronesis'', vol. 40, 1995, p. 125-142, ici p. 138-141).</ref>. Le quatrième attribut, le contrôle exercé sur tous les êtres animés, s'accorde avec B11, selon lequel l'Intellect est présent dans certaines choses. Schofield montre que tout le passage peut se lire aussi bien comme une description d'un Intellect suprême que comme une thèse sur l'intellect en général, et que cette ambiguïté est probablement inhérente au texte<ref>Schofield, 1980, p. 10-22 ; Curd, 2007, p. 60-61.</ref>.
=== Le rôle cosmogonique du Noûs ===
B12 décrit ensuite le rôle du Noûs dans le déclenchement et l'extension de la révolution cosmique<ref>Anaxagore, B12 (DK 59 B 12) ; Curd, 2007, p. 62-66 et 193-205.</ref> :
<blockquote>Et l'Intellect a dominé la révolution entière, de sorte qu'elle a commencé à tourner au commencement. Elle a d'abord commencé à tourner à partir d'une petite région, mais elle tourne sur une région plus grande, et elle tournera sur une région plus grande encore. Et les choses qui se mêlent, qui se séparent et qui se dissocient, l'Intellect les a toutes connues. Et celles qui devaient être, celles qui étaient et ne sont plus, celles qui sont maintenant et celles qui seront, l'Intellect les a toutes ordonnées, ainsi que cette révolution dans laquelle tournent maintenant les astres, le soleil, la lune, l'air et l'éther qui se séparent.<ref>Anaxagore, B12, cité par Simplicius, ''Commentaire sur la Physique'', 156, 24-157, 2.</ref></blockquote>
Ce passage affirme trois choses. L'Intellect initie une révolution (περιχώρησις) dans un mélange jusqu'alors immobile. Ce mouvement, commencé dans une petite région, s'étend progressivement et continue de s'étendre : la cosmogonie n'est pas un événement révolu, mais un processus qui se poursuit à la périphérie du mélange illimité. Enfin, c'est ce mouvement qui produit la séparation (ἀπόκρισις) et la dissociation (διάκρισις) des ingrédients, d'où naît le cosmos ordonné que nous observons<ref>Anaxagore, B12 et B13 ; Simplicius, ''Commentaire sur la Physique'', 300, 27-301, 1 ; Aristote, ''Physique'', VIII, 1, 250b24-26.</ref>.
Le mécanisme de la séparation est d'ordre physique : la rotation, par sa vitesse et sa force, rassemble au centre les ingrédients denses, humides, froids et obscurs, et repousse vers la périphérie les ingrédients rares, chauds, secs et brillants<ref>Anaxagore, B9, B12, B15 ; Aristote, ''Du ciel'', II, 13, 295a9-14 (DK 59 A 88) ; Curd, 2007, p. 207-208.</ref>. Dans les explications particulières conservées, les phénomènes sont ensuite généralement rapportés aux effets physiques de cette rotation et aux propriétés des ingrédients. Platon et Aristote reprochent précisément à Anaxagore de faire peu d'usage explicite du Noûs dans ces explications ; Simplicius répond que le Noûs demeure la cause première du mouvement dont procèdent les séparations<ref>Platon, ''Phédon'', 97b-98c (DK 59 A 47) ; Aristote, ''Métaphysique'', A, 4, 985a18-21 (DK 59 A 47) ; Simplicius, ''Commentaire sur la Physique'', 300, 27-31 ; Curd, 2007, p. 202-203 et 206-207.</ref>.
Le texte affirme aussi que « l'Intellect a ordonné » (διεκόσμησε νοῦς) toutes choses, passées, présentes et futures. Le verbe διακοσμεῖν signifie « disposer », « mettre en ordre ». La portée téléologique de cette ordonnance reste débattue. La révision 2026 de l'article de Curd et Sisko dans la ''Stanford Encyclopedia of Philosophy'' distingue des lectures téléologiques « minces », qui relient cognition, volition et mouvement, et des lectures « épaisses », qui attribuent au Noûs des buts déterminés ; elle classe notamment Curd, Lesher et Pinto parmi les premières, Laks et Sedley parmi les secondes<ref>Curd et Sisko, ''SEP'', 2026, § 4.2 ; Lesher, 1995 ; Curd, 2007, p. 194 et 204-205 ; Rhodes Pinto, « ''Nous'', Motion, and Teleology in Anaxagoras », ''Oxford Studies in Ancient Philosophy'', vol. 52, 2017, p. 1-32 ; André Laks, « Mind's Crisis: On Anaxagoras' ''Nous'' », ''Southern Journal of Philosophy'', vol. 31, suppl., 1993, p. 19-38 ; Sedley, 2007 ; Marmodoro, 2017, p. 129-130.</ref>. D'autres interprètes comprennent le Noûs principalement comme principe de mouvement ou comme loi naturelle plutôt que comme agent poursuivant une fin<ref>Sider, 1981, selon Curd et Sisko, ''SEP'', 2026, § 4.1-4.2 ; Gershenson et Greenberg, 1964, p. 25 ; John E. Sisko, « Anaxagoras betwixt Parmenides and Plato », ''Philosophy Compass'', vol. 5, 2010, p. 432-442.</ref>. Curd refuse en particulier d'attribuer au Noûs un bien indépendant que l'ordre cosmique aurait pour fin d'atteindre, tout en lui reconnaissant connaissance, direction et organisation du processus cosmique<ref>Curd, 2007, p. 194 et 204-205.</ref>. Kurt von Fritz voyait quant à lui dans le Noûs la jonction difficile de l'intelligence cognitive héritée de Parménide et du principe moteur de la tradition milésienne ; Laks et DeFilippo ont poursuivi le débat sur la finalité de ce principe<ref>Kurt von Fritz, « Der ΝΟΥΣ des Anaxagoras », ''Archiv für Begriffsgeschichte'', vol. 9, 1964, p. 87-102 ; Joseph G. DeFilippo, « Reply to André Laks on Anaxagoras' ΝΟΥΣ », ''Southern Journal of Philosophy'', vol. 31, suppl., 1993, p. 39-48 ; Curd et Sisko, ''SEP'', 2026, § 4.2.</ref>.
=== La critique platonicienne et aristotélicienne ===
Dans le ''Phédon'', Platon fait raconter par Socrate sa déception à la lecture du livre d'Anaxagore<ref>Platon, ''Phédon'', 97b-98c (DK 59 A 47).</ref> :
<blockquote>Un jour, j'entendis quelqu'un lire dans un livre d'Anaxagore, disait-il, que c'est l'Intellect qui met tout en ordre et qui est la cause de toutes choses. Cette cause me réjouit, et il me sembla qu'il était bon, d'une certaine manière, que l'Intellect fût la cause de tout ; et je pensai que, s'il en est ainsi, l'Intellect, en ordonnant, ordonne tout et dispose chaque chose de la manière qui est la meilleure. [...] Mais cette merveilleuse espérance, mon ami, me fut ôtée lorsque, poursuivant ma lecture, je vis un homme qui ne faisait aucun usage de l'Intellect, qui ne lui attribuait aucune responsabilité dans l'ordonnance des choses, mais qui alléguait comme causes des airs, des éthers, des eaux et bien d'autres choses étranges.<ref>Platon, ''Phédon'', 97b-98c (DK 59 A 47).</ref></blockquote>
Le passage distingue l'attente du Socrate platonicien de ce qu'il dit effectivement avoir trouvé chez Anaxagore : il espérait une explication de l'ordre par « le meilleur », puis déclare ne pas l'avoir rencontrée dans sa lecture<ref>Platon, ''Phédon'', 97b-98c (DK 59 A 47) ; Curd, 2007, p. 144-145 et 204-205.</ref>.
Le texte d'Anaxagore, dans les fragments que nous possédons, affirme que le Noûs connaît toutes choses et qu'il les ordonne par l'intermédiaire de la révolution cosmique. Il n'affirme pas que cet ordre soit le meilleur possible. C'est Socrate qui, lisant Anaxagore, infère que, si le monde est ordonné par un Intellect, il doit l'être en vue du bien. Curd juge que Platon a raison de constater l'absence, chez Anaxagore, d'un bien indépendant qui servirait de principe d'explication ; elle note que cette exigence, chez Platon, conduira à la Forme du Bien, puis au Démiurge du ''Timée''<ref>Curd, 2007, p. 144-145 et 204.</ref>. Il faut donc distinguer l'attribution au Noûs d'un rôle moteur et cognitif, qui est anaxagoréenne, et celle d'un principe évaluatif selon lequel le monde serait disposé en vue du meilleur, qui ne se trouve pas dans les fragments. Le reproche socratique ne dénonce pas une incohérence interne ; il regrette qu'Anaxagore ne soit pas allé jusqu'à l'explication que Socrate attendait.
Aristote reprend, dans la ''Métaphysique'', le reproche d'un usage insuffisant du Noûs<ref>Aristote, ''Métaphysique'', A, 4, 985a18-21 (DK 59 A 47).</ref> :
<blockquote>Anaxagore se sert de l'Intellect comme d'un ''deus ex machina'' pour la fabrication du monde ; et quand il est embarrassé pour dire par quelle cause une chose est nécessairement, il le fait intervenir ; mais dans les autres cas, il donne pour causes de ce qui arrive toutes choses plutôt que l'Intellect.<ref>Aristote, ''Métaphysique'', A, 4, 985a18-21 (DK 59 A 47).</ref></blockquote>
Aristote précise ailleurs que ceux qui posent l'Intellect ou l'Amitié comme causes les traitent comme des principes du mouvement plutôt que comme des fins<ref>Aristote, ''Métaphysique'', A, 7, 988b6-11.</ref>. Curd souligne que les critiques aristotéliciennes portent aussi sur le rapport entre âme et intellect et sur les conditions de la connaissance du Noûs<ref>Aristote, ''De l'âme'', I, 2, 404a25-b6 et 405a13-19 (DK 59 A 55, A 99, A 100) ; III, 4, 429a18-24 et 429b22-24 (DK 59 A 100) ; Curd, 2007, p. 146 et 205.</ref>. Les fragments ne formulent pas l'explication « par le meilleur » attendue par le Socrate du ''Phédon'', mais ce constat n'épuise pas le débat moderne sur l'existence d'une téléologie proprement anaxagoréenne<ref>Curd et Sisko, ''SEP'', 2026, § 4.2.</ref>.
=== Synthèse : l'innovation du Noûs ===
Les spécialistes ont souligné plusieurs innovations liées à l'introduction du Noûs, tout en discutant leur portée exacte<ref>Curd, 2007, p. 192-205 ; Schofield, 1980, p. 4-22 ; Curd et Sisko, ''SEP'', 2026, § 4.</ref>.
Anaxagore est, parmi les penseurs grecs dont nous connaissons les doctrines, le premier à poser explicitement une entité qui, bien qu'elle agisse sur les ingrédients, n'est mêlée à aucun d'eux<ref>Guthrie, 1965, p. 276-279 ; Curd, 2007, p. 193-194 et 200-201. Aristote (''Métaphysique'', A, 3, 984b15-20 = DK 59 A 58) signale que la doctrine était attribuée avant lui à Hermotime de Clazomènes, figure largement légendaire (Curd, 2007, p. 205, n. 23).</ref>. Il s'inscrit néanmoins dans une tradition : Curd rapproche l'idée d'une intelligence cosmique de certains motifs présents chez Xénophane et Héraclite<ref>Curd, 2007, p. 195 et n. 6-7.</ref>. Les spécialistes ont également rapproché plusieurs traits du Noûs de développements ultérieurs chez Platon et Aristote<ref>Curd, 2007, p. 143-146 ; Curd et Sisko, ''SEP'', 2026, § 6.</ref>.
Anaxagore identifie par ailleurs le Noûs comme principe du mouvement cosmique et de l'ordre produit par la révolution. Aristote salue cette innovation en disant qu'Anaxagore parut « comme un homme sobre » au milieu de prédécesseurs qui parlaient au hasard<ref>Aristote, ''Métaphysique'', A, 3, 984b15-20 (DK 59 A 58).</ref>. Les textes de Platon et d'Aristote reprendront et transformeront profondément la question du rapport entre intellect et ordre cosmique<ref>Schofield, 1980, p. 59-61 ; Curd, 2007, p. 143-146 ; Curd et Sisko, ''SEP'', 2026, § 6.</ref>.
== La cosmogonie et la cosmologie ==
La cosmogonie et la cosmologie d'Anaxagore appliquent ses principes métaphysiques. Anaxagore s'y montre héritier de la tradition ionienne et novateur, proposant des explications naturalistes des phénomènes célestes et météorologiques<ref>Curd, 2007, p. 206-234 ; Gershenson et Greenberg, 1964, p. 34-54 ; Daniel W. Graham, ''Science before Socrates: Parmenides, Anaxagoras, and the New Astronomy'', New York, Oxford University Press, 2013.</ref>. Les témoignages sont ici notre source principale, et ils ne concordent pas toujours.
=== Le mouvement rotatoire cosmogonique ===
Le processus cosmogonique commence lorsque le Noûs imprime au mélange originel un mouvement de révolution (περιχώρησις)<ref>Anaxagore, B12 et B13 (DK 59 B 12-13).</ref>. Ce mouvement prend la forme d'un tourbillon (δίνη) dont l'étendue ne cesse de croître<ref>Aristote, ''Du ciel'', II, 13, 295a9-14 (DK 59 A 88) ; Curd, 2007, p. 207-208.</ref>. Anaxagore affirme que sa rapidité est sans commune mesure avec celle d'aucune chose connue des hommes : elle est « de beaucoup de fois plus rapide »<ref>Anaxagore, fragment B9 (DK 59 B 9).</ref>.
La séparation qui en résulte est d'ordre physique : le tourbillon rassemble au centre les ingrédients denses et lourds et repousse vers la périphérie les ingrédients rares et légers ; selon Aristote, les partisans du tourbillon tiraient cet argument de ce qu'on observe dans l'eau et dans l'air<ref>Aristote, ''Du ciel'', II, 13, 295a9-14 (DK 59 A 88), qui rapporte que, dans les tourbillons des liquides et de l'air, les corps plus grands et plus lourds se portent vers le centre ; Hippolyte, ''Réfutation'', I, 8, 2 (DK 59 A 42).</ref>. Le fragment B15 décrit ce processus :
<blockquote>Le dense, l'humide, le froid et l'obscur se rassemblèrent ici, là où se trouve maintenant la terre ; le rare, le chaud et le sec se retirèrent vers les régions lointaines de l'éther.<ref>Anaxagore, fragment B15, cité par Simplicius, ''Commentaire sur la Physique'', 179, 3-6 (DK 59 B 15). Hippolyte ajoute « le brillant » à la seconde série.</ref></blockquote>
Cette séparation n'est jamais achevée : conformément au principe selon lequel rien n'est complètement séparé (B8, B12), la rotation continue de produire des séparations et des mélanges partiels<ref>Anaxagore, B8 (« les choses qui sont dans l'unique cosmos ne sont pas séparées les unes des autres ni tranchées à la hache ») et B12.</ref>. L'ordre exact des premières séparations, celle de l'air et de l'éther puis celle des opposés et de la terre, reste discuté<ref>D. Bargrave-Weaver, « The Cosmogony of Anaxagoras », ''Phronesis'', vol. 4, 1959, p. 77-91 ; M. C. Stokes, « On Anaxagoras, Part II: The Order of Cosmogony », ''Archiv für Geschichte der Philosophie'', vol. 47, 1965, p. 217-250 ; Ronald Potts, « Anaxagoras' Cosmogony », ''Apeiron'', vol. 18, 1984, p. 90-96.</ref>.
=== La formation de la terre ===
Au centre du tourbillon, les ingrédients denses, humides, froids et obscurs se sont concentrés pour former la terre<ref>Anaxagore, B15 et B16 ; Hippolyte, ''Réfutation'', I, 8, 2 (DK 59 A 42).</ref>. Selon les témoignages, Anaxagore tenait la terre pour plate<ref>Hippolyte, ''Réfutation'', I, 8, 3 (DK 59 A 42) ; Aristote, ''Du ciel'', II, 13, 294b13-21 (DK 59 A 87).</ref>.
Elle demeure immobile parce qu'elle repose sur l'air qui la supporte. Selon Aristote, Anaximène, Anaxagore et Démocrite expliquaient cette stabilité par la platitude de la terre : elle ne fend pas l'air situé au-dessous, mais le recouvre comme un couvercle<ref>Aristote, ''Du ciel'', II, 13, 294b13-21 (DK 59 A 87). Hippolyte (I, 8, 3 = DK 59 A 42) invoque aussi la grandeur de la terre et l'absence de vide.</ref>. Aristote rapporte qu'Anaxagore montrait la résistance de l'air au moyen d'outres gonflées et de clepsydres<ref>Aristote, ''Physique'', IV, 6, 213a22-27 (DK 59 A 68) ; pseudo-Aristote, ''Problèmes'', XVI, 8, 914b9-915a24 (DK 59 A 69) ; Gershenson et Greenberg, 1964, p. 40-43. La clepsydre désigne ici un ustensile servant à transvaser les liquides (Curd, 2007, p. 108, n. 28).</ref>.
=== La formation des corps célestes ===
Les corps célestes se seraient formés à partir de pierres arrachées à la terre par la violence de la rotation, puis enflammées par l'éther brûlant qui les emporte<ref>Aétius, II, 13, 3 (DK 59 A 71) ; Hippolyte, ''Réfutation'', I, 8, 6 (DK 59 A 42) ; Curd, 2007, p. 209.</ref>. Les astres ne sont donc pas des êtres divins, mais des pierres incandescentes<ref>Platon, ''Apologie'', 26d (DK 59 A 35) ; Hippolyte, ''Réfutation'', I, 8, 6 (DK 59 A 42). Cette thèse est au cœur des accusations d'impiété.</ref>.
Anaxagore soutenait que le soleil est une masse de métal incandescent ou une pierre de feu, plus grande que le Péloponnèse, et même plusieurs fois plus grande selon Aétius ; Plutarque lui attribue aussi l'idée que la lune a la taille du Péloponnèse<ref>Diogène Laërce, II, 8 (DK 59 A 1) ; Hippolyte, ''Réfutation'', I, 8, 8 (DK 59 A 42) ; Aétius, II, 20, 6 et II, 21, 3 (DK 59 A 72) ; Plutarque, ''Sur la face qui paraît dans la lune'', 932a ; Daniel W. Graham et Eric Hintz, « Anaxagoras and the Solar Eclipse of 478 BC », ''Apeiron'', vol. 40, 2007, p. 319-344, ici p. 319.</ref>. David Sider, à la suite d'Erich Frank, a cherché l'origine de l'estimation solaire dans l'éclipse du 30 avril 463 ; Daniel Graham et Eric Hintz ont proposé l'éclipse annulaire du 17 février 478. Ces reconstructions restent hypothétiques<ref>David Sider, « Anaxagoras on the Size of the Sun », ''Classical Philology'', vol. 68, 1973, p. 128-129 ; Graham et Hintz, 2007, p. 321-330 ; Graham, ''Science before Socrates'', 2013 ; Curd et Sisko, ''SEP'', 2026, § 1.</ref>.
La lune, selon Anaxagore, est faite de terre, avec des plaines et des ravins<ref>Hippolyte, ''Réfutation'', I, 8, 10 (DK 59 A 42) ; Diogène Laërce, II, 8 (DK 59 A 1) ; Aétius, II, 30, 2 (DK 59 A 77).</ref>. Hippolyte rapporte qu'elle n'a pas de lumière propre et reçoit celle du soleil ; le fragment B18 exprime lui aussi une dépendance de la lumière lunaire à l'égard du soleil, mais son authenticité a été discutée<ref>Hippolyte, ''Réfutation'', I, 8, 8 (DK 59 A 42) ; Anaxagore, B18, cité par Plutarque, ''Sur la face qui paraît dans la lune'', 16, 929b (DK 59 B 18) ; Platon, ''Cratyle'', 409a-b (DK 59 A 76) ; Curd, 2007, p. 73-74 et 211, n. 15.</ref>. Aétius attribue toutefois l'idée de l'illumination solaire de la lune à plusieurs penseurs, depuis Thalès, Pythagore et Parménide ; Daniel Graham en crédite Parménide, tandis que Dirk Couprie conteste que le rapport décrit chez Anaxagore doive être compris comme une simple réflexion<ref>Aétius, II, 28, 5 (DK 59 A 77) ; Parménide, DK 28 B 14-15 ; Daniel W. Graham, « La lumière de la lune dans la pensée grecque archaïque », dans André Laks et Claire Louguet (éd.), ''Qu'est-ce que la philosophie présocratique ?'', Lille, Presses universitaires du Septentrion, 2002, p. 351-380 ; Denis O'Brien, « Derived Light and Eclipses in the Fifth Century », ''Journal of Hellenic Studies'', vol. 88, 1968, p. 114-127 ; Dirk L. Couprie, « Anaxagoras on the Light and Phases of the Moon », ''Hyperboreus'', vol. 24, 2018, p. 12-39 ; Curd, 2007, p. 211 et n. 15.</ref>.
Hippolyte crédite Anaxagore d'avoir le premier expliqué les éclipses et les phases de la lune. Une éclipse de lune se produit lorsque la terre s'interpose entre le soleil et la lune ; une éclipse de soleil, lors de la nouvelle lune, lorsque la lune s'interpose entre le soleil et la terre<ref>Hippolyte, ''Réfutation'', I, 8, 9-10 (DK 59 A 42) ; Aétius, II, 29, 6-7 (DK 59 A 77) ; Plutarque, ''Vie de Nicias'', 23 (DK 59 A 18).</ref>. Les témoignages ajoutent cependant que certaines éclipses de lune pourraient être causées par des corps invisibles situés au-dessous de la lune. Curd, ainsi que Graham et Hintz, retiennent l'explication par l'interposition de la terre comme un élément important et pour l'essentiel correct de la théorie ; Couprie propose au contraire une reconstruction dans laquelle les corps invisibles jouent le rôle principal dans l'explication anaxagoréenne des éclipses lunaires<ref>Hippolyte, ''Réfutation'', I, 8, 6 et 9 (DK 59 A 42) ; Aétius, II, 29, 7 (DK 59 A 77) ; Curd, 2007, p. 211-212 ; Graham et Hintz, 2007, p. 320 ; Dirk L. Couprie, « Anaxagoras on the Milky Way and Lunar Eclipses », ''Hyperboreus'', vol. 23, 2017, p. 181-207.</ref>.
La tradition prête aussi à Anaxagore des prédictions, dont celle d'une éclipse de soleil<ref>Philostrate, ''Vie d'Apollonios de Tyane'', I, 2 (DK 59 A 6) ; cf. Hippolyte, ''Réfutation'', I, 8, 13 (DK 59 A 42), qui le dit « devin ».</ref>. Curd traite ces récits de prédiction avec scepticisme et souligne leur caractère tardif ou légendaire<ref>Curd, 2007, p. 132, n. 9-10.</ref>.
La Voie lactée s'explique, selon Anaxagore, par l'ombre que projette la terre lorsque le soleil passe au-dessous d'elle : les étoiles situées dans cette ombre, que le soleil n'éclaire pas, laissent voir leur propre lumière<ref>Aristote, ''Météorologiques'', I, 8, 345a25-31 (DK 59 A 80) ; Aétius, III, 1, 5 ; Hippolyte, ''Réfutation'', I, 8, 10 (DK 59 A 42) ; Curd, 2007, p. 210 ; Couprie, 2017.</ref>.
=== La météorite d'Aigos Potamos ===
La chute d'une grande météorite à Aigos Potamos, sur la rive européenne de l'Hellespont, vers 467 av. J.-C., fut très tôt associée au nom d'Anaxagore<ref>Marbre de Paros, ép. 57, et Pline l'Ancien, ''Histoire naturelle'', II, 149 (DK 59 A 11) ; Plutarque, ''Vie de Lysandre'', 12 (DK 59 A 12) ; Diogène Laërce, II, 10 (DK 59 A 1). Le Marbre de Paros donne 468/7, Pline la deuxième année de la 78{{e}} Olympiade (467/6).</ref>. Plusieurs sources affirment qu'il l'avait prédite. Burnet jugeait cette prédiction absurde, et Curd rappelle qu'une chute particulière, en un lieu et à un moment déterminés, ne pouvait être prévue à partir de la théorie rapportée<ref>Burnet, 1930, chap. VI ; Curd, 2007, p. 132 et n. 10.</ref>. Selon Plutarque, Anaxagore enseignait que les astres, pierres lourdes maintenues par la force de la révolution, pouvaient tomber si celle-ci se relâchait ; Daniel Graham comprend la « prédiction » comme une anticipation générale de la possibilité de chutes de pierres célestes, et non comme l'annonce d'un événement particulier<ref>Plutarque, ''Vie de Lysandre'', 12 (DK 59 A 12) ; Daniel W. Graham, « Anaxagoras and the Comet », ''Ancient Philosophy'', vol. 33, 2013, p. 1-18 ; Evangelos Th. Theodossiou, P. G. Niarchos, V. N. Manimanis et Wayne Orchiston, « The Fall of a Meteorite at Aegos Potami in 467/466 BC », ''Journal of Astronomical History and Heritage'', vol. 5, 2002, p. 135-140.</ref>. Plutarque rapporte aussi, d'après Daïmachos, qu'un corps enflammé fut observé pendant soixante-quinze jours avant la chute<ref>Plutarque, ''Vie de Lysandre'', 12 (DK 59 A 12) ; Curd, 2007, p. 132.</ref>. Pline dit qu'Anaxagore avait annoncé la chute d'une pierre venue du soleil ; P. J. Bicknell a proposé que ce récit puisse garder le souvenir d'une observation de tache solaire, hypothèse distincte du témoignage antique lui-même<ref>Pline l'Ancien, ''Histoire naturelle'', II, 149 (DK 59 A 11) ; P. J. Bicknell, « Did Anaxagoras Observe a Sunspot in 467 B.C.? », ''Isis'', vol. 59, 1968, p. 87-90.</ref>.
=== Météorologie ===
Anaxagore expliquait les nuages et la neige à peu près comme Anaximène<ref>Aétius, III, 4, 2 (DK 59 A 85) ; Curd, 2007, p. 223.</ref>. Le fragment B16 décrit une série de solidifications : des nuages se sépare l'eau, de l'eau la terre, et de la terre les pierres, que le froid solidifie<ref>Anaxagore, fragment B16 (DK 59 B 16).</ref>.
La formation de la grêle posait un problème particulier : comment de la glace peut-elle se former en été ? Selon Aristote, qui critique cette théorie, Anaxagore soutenait que la grêle se forme lorsqu'un nuage est poussé vers la région supérieure, plus froide ; les chaleurs de l'été, en poussant les nuages plus haut, expliqueraient la fréquence des orages de grêle en été et dans les pays chauds<ref>Aristote, ''Météorologiques'', I, 12, 348a14-20 et 348b12-16 (DK 59 A 85), Aétius, III, 4, 2 (DK 59 A 85), qui ajoute que les gouttes s'arrondissent en tombant ; Curd, 2007, p. 223.</ref>.
Le texte transmis comme fragment B19 attribue à Anaxagore cette proposition : « Nous appelons arc-en-ciel le reflet du soleil dans les nuages. » L'étendue du fragment est toutefois discutée : à la suite de Friedrich Solmsen, la plupart des éditeurs modernes ne lui attribuent plus la phrase suivante de la scholie, qui fait de l'arc-en-ciel un signe de tempête et explique le vent ou la pluie<ref>Anaxagore, B19, conservé par les scholies BT à l’''Iliade'', XVII, 547 (DK 59 B 19) ; Friedrich Solmsen, « Anaxagoras B 19 Diels-Kranz », ''Hermes'', vol. 91, 1963, p. 251-252 ; Curd, 2007, p. 74 et 223.</ref>.
Selon la version transmise par Aristote, Aétius et Sénèque, le tonnerre et l'éclair sont produits lorsqu'une composante ignée venue de l'éther pénètre dans les nuages : son éclat produit l'éclair et le bruit de son extinction le tonnerre<ref>Aristote, ''Météorologiques'', II, 9, 369b14-19 (DK 59 A 84) ; Aétius, III, 3, 4 (DK 59 A 84) ; Sénèque, ''Questions naturelles'', II, 12, 3 et II, 19 (DK 59 A 84) ; Curd, 2007, p. 223-224.</ref>. Aristote précise qu'Anaxagore tenait l'éclair pour réellement antérieur au tonnerre ; l'explication de ce décalage par une différence de vitesse de propagation appartient à Aristote, non à Anaxagore<ref>Aristote, ''Météorologiques'', II, 9, 369b7-19.</ref>.
Les tremblements de terre sont attribués, selon Aristote, à l'éther qui, tendant naturellement vers le haut, se trouve emprisonné dans les cavités souterraines ; Aétius et Hippolyte parlent plutôt d'air qui pénètre sous la terre et l'ébranle<ref>Aristote, ''Météorologiques'', II, 7, 365a19-35 (DK 59 A 89) ; Aétius, III, 15, 4 (DK 59 A 89) ; Hippolyte, ''Réfutation'', I, 8, 12 (DK 59 A 42).</ref>.
=== Hydrologie ===
Anaxagore expliquait la crue estivale du Nil par la fonte des neiges dans les régions montagneuses d'Éthiopie<ref>Hippolyte, ''Réfutation'', I, 8, 5 (DK 59 A 42) ; Aétius, IV, 1, 3 (DK 59 A 91) ; Sénèque, ''Questions naturelles'', IV a, 2, 17 (DK 59 A 91) ; Schofield, 1980, p. 34.</ref>. Hérodote jugeait déjà cette explication séduisante mais fausse, et Sénèque la rejette. La localisation éthiopienne correspond bien à une région majeure d'alimentation du Nil, mais l'hydrologie moderne attribue le régime saisonnier du Nil Bleu aux pluies des hauts plateaux éthiopiens, concentrées principalement de juin à septembre, et non à la fonte des neiges<ref>Hérodote, II, 22 (DK 59 A 91) ; Curd, 2007, p. 225 ; Declan Conway, « The Climate and Hydrology of the Upper Blue Nile River », ''The Geographical Journal'', vol. 166, no 1, 2000, p. 49-62.</ref>. Quant à la mer, Aétius attribue à Anaxagore l'idée qu'après évaporation de la partie la plus fine de l'eau primitive, le résidu devint salé et amer<ref>Aétius, III, 16, 2 (DK 59 A 90) ; Hippolyte, ''Réfutation'', I, 8, 4 (DK 59 A 42) ; Curd, 2007, p. 225.</ref>.
=== D'autres mondes ? ===
Le fragment B4a affirme que des hommes et d'autres animaux se sont composés, qu'il existe des cités habitées et des champs cultivés « comme chez nous », un soleil, une lune et d'autres astres « comme chez nous » ; Anaxagore précise qu'il a dit cela de la séparation parce qu'elle se produirait non seulement chez nous, mais aussi ailleurs<ref>Anaxagore, B4a (DK 59 B 4a), cité par Simplicius, ''Commentaire sur la Physique'', 34, 29-35, 9, et commenté en 157, 9-24.</ref>. Le sens de cet « ailleurs » est discuté. Hermann Fränkel y voyait une expérience de pensée. Jaap Mansfeld objecte que le texte ne présente pas cet autre monde comme hypothétique ; s'appuyant sur B3 et B6, il propose d'y voir un monde infiniment petit, situé au-dessous du seuil de la perception et en tout semblable au nôtre, parce qu'il est fait des mêmes composants<ref>Jaap Mansfeld, « Anaxagoras' Other World », ''Phronesis'', vol. 25, 1980, p. 1-4, ici p. 2-3, qui discute aussi la lecture de Fränkel, reprend une idée de P. Leon (1927) et compare ces mondes emboîtés à l'image répétée à l'infini d'une boîte de chocolats Droste. Aétius (II, 1, 2 = DK 59 A 63) range d'ailleurs Anaxagore parmi les partisans d'un monde unique.</ref>. D'autres interprètes y voient des mondes séparés dans l'espace, ou des tourbillons secondaires en bordure du tourbillon principal ; John Sisko propose des mondes emboîtés vers le petit comme vers le grand<ref>Curd, 2007, p. 213 ; Curd et Sisko, ''SEP'', 2026, § 4.3 ; Sisko, 2003, p. 87-114.</ref>.
=== Synthèse : la cosmologie naturaliste d'Anaxagore ===
Les points suivants résument les traits que les fragments, les témoignages et les principales reconstructions modernes attribuent à la cosmologie d'Anaxagore<ref>Curd, 2007, p. 206-234 ; Graham, ''Science before Socrates'', 2013 ; Curd et Sisko, ''SEP'', 2026, § 5.</ref>.
Le premier est l'unité de la nature : les corps célestes sont faits des mêmes ingrédients que la terre, puisque ce sont des pierres arrachées à celle-ci<ref>Aétius, II, 13, 3 (DK 59 A 71) ; Hippolyte, ''Réfutation'', I, 8, 6 (DK 59 A 42) ; Gershenson et Greenberg, 1964, p. 47.</ref>. Il n'existe pas de différence de nature entre le monde sublunaire et le ciel, contrairement à ce qu'affirmera Aristote<ref>Aristote, ''Du ciel'', I, 2-3, 268b11-270b25.</ref>.
Le deuxième trait est le recours à des explications internes au système naturel : après l'impulsion initiale du Noûs, les témoignages rapportent les phénomènes cosmologiques et météorologiques à la rotation, aux séparations, aux mouvements de l'air et de l'éther, à l'évaporation ou à la condensation plutôt qu'à des interventions ponctuelles des dieux traditionnels<ref>Curd, 2007, p. 206-225 ; Curd et Sisko, ''SEP'', 2026, § 2 et 5.</ref>.
Gershenson et Greenberg ont particulièrement insisté sur l'usage anaxagoréen de l'analogie entre phénomènes observables et phénomènes à expliquer ; Curd souligne également le rôle de l'expérience sensible comme indice des processus invisibles<ref>Gershenson et Greenberg, 1964, p. 24 et 34 ; Curd, 2007, p. 229-233.</ref>.
Gershenson et Greenberg ont proposé de faire d'Anaxagore le premier savant au sens moderne et d'identifier chez lui la plupart des éléments de la « méthode scientifique »<ref>Gershenson et Greenberg, 1964, préface et p. 58-59.</ref>. Cette caractérisation est une reconstruction historiographique forte ; pour une perspective plus prudente, on se reportera à l'histoire de la science grecque de G. E. R. Lloyd<ref>G. E. R. Lloyd, ''Early Greek Science: Thales to Aristotle'', Londres, Chatto & Windus, 1970.</ref>.
== La physiologie et la biologie ==
Bien qu'Anaxagore soit surtout connu pour sa cosmologie, les témoignages indiquent qu'il s'intéressa aussi à la nutrition, à la perception, à l'embryologie et aux êtres vivants<ref>Curd, 2007, p. 225-229 ; Gershenson et Greenberg, 1964, p. 55-57 ; Hubert Erhard, « Anaxagoras als Biologe », ''Sudhoffs Archiv für Geschichte der Medizin und der Naturwissenschaften'', vol. 35, 1942, p. 117-140.</ref>.
=== La théorie de la nutrition ===
L'une des questions biologiques qu'Anaxagore cherche à élucider est celle de la nutrition : comment une nourriture simple, comme le pain et l'eau, peut-elle nourrir les cheveux, les veines, les artères, la chair, les nerfs et les os ?<ref>Aétius, I, 3, 5 (DK 59 A 46) ; Simplicius, ''Commentaire sur la Physique'', 460, 4-20 (DK 59 A 45) ; Platon, ''Phédon'', 96c-d (DK 59 A 46) ; Lucrèce, ''De la nature'', I, 830-920 (DK 59 A 44).</ref> Cette observation pose un problème au regard de l'interdit parménidien : une substance nouvelle, la chair, semble naître de ce qui n'est pas chair.
La réponse d'Anaxagore est cohérente avec sa métaphysique : le pain doit déjà contenir de la chair, du sang, des os et toutes les autres substances corporelles, en proportions si faibles qu'elles restent imperceptibles<ref>Simplicius, ''Commentaire sur la Physique'', 460, 4-20 (DK 59 A 45), qui ajoute que, si les arbres se nourrissent d'eau, l'eau doit contenir du bois, de l'écorce et des fruits.</ref>. Lorsque nous mangeons, la chair contenue dans la nourriture vient s'ajouter à la chair du corps<ref>Aétius, I, 3, 5 (DK 59 A 46) ; Simplicius, ''Commentaire sur la Physique'', 460, 4-20 (DK 59 A 45).</ref>.
Cette théorie soulève une autre difficulté : comment le corps dirige-t-il la chair vers les muscles et les parties osseuses vers les os ? Les fragments ne le disent pas. Certains interprètes supposent qu'Anaxagore rattachait cette fonction au Noûs présent dans les êtres vivants<ref>Gershenson et Greenberg, 1964, p. 24, pour qui la digestion est, chez Anaxagore, une fonction de l'intellect ; Curd (2007, p. 175-177) suggère que les semences contiennent, par le Noûs ou l'âme, un principe d'organisation, tout en qualifiant cette reconstruction de spéculative.</ref>.
=== La théorie de la perception ===
Anaxagore élabora une théorie de la perception fondée sur le principe que le semblable n'est pas affecté par le semblable : la perception se fait par les contraires<ref>Théophraste, ''Du sens'', 27 (DK 59 A 92).</ref>. Il s'opposait ainsi à Empédocle, pour qui la perception se fait par le semblable<ref>Théophraste, ''Du sens'', 1-2 (DK 31 A 86).</ref>.
Pour qu'une perception ait lieu, il doit exister une différence entre l'organe et l'objet perçu : ce qui est exactement aussi chaud ou aussi froid que nous ne nous réchauffe ni ne nous refroidit à son contact<ref>Théophraste, ''Du sens'', 28 (DK 59 A 92).</ref>. La vision se fait par le reflet de l'objet dans la pupille, qui ne se produit que sur une couleur différente<ref>Théophraste, ''Du sens'', 27 (DK 59 A 92) ; Gershenson et Greenberg, 1964, p. 28-32.</ref>.
Anaxagore en concluait que toute perception s'accompagne de douleur ou de gêne (λύπη), car tout contact du dissemblable produit une irritation, sensible lorsque la sensation est trop longue ou trop intense<ref>Théophraste, ''Du sens'', 29 (DK 59 A 92) ; Aétius, IV, 9, 16, et Aristote, ''Éthique à Nicomaque'', VII, 15, 1154b7-9 (DK 59 A 94) ; James Warren, « Anaxagoras on Perception, Pleasure, and Pain », ''Oxford Studies in Ancient Philosophy'', vol. 33, 2007, p. 19-54 ; Curd, 2007, p. 169 et n. 37.</ref>.
=== La reproduction et l'embryologie ===
Les témoignages sur l'embryologie d'Anaxagore ne concordent pas entièrement. Selon Aristote, il comptait parmi ceux pour qui la différence des sexes est déjà présente dans la semence : la semence vient du mâle, la femelle ne fournissant que le lieu, et les mâles proviennent du côté droit, les femelles du côté gauche<ref>Aristote, ''Génération des animaux'', IV, 1, 763b30-33 (DK 59 A 107) ; Hippolyte, ''Réfutation'', I, 8, 12 (DK 59 A 42). Schofield (1980, p. 34) relève que l'idée selon laquelle la mère ne fournit qu'un lieu apparaît dans les ''Euménides'' d'Eschyle (657-666).</ref>. D'autres sources lui prêtent l'idée d'une contribution séminale des deux parents, l'enfant ressemblant à celui qui a fourni le plus de semence<ref>Censorinus, ''Du jour natal'', 5, 2-4 et 6, 6-8 (DK 59 A 107 et A 111) ; Aétius, V, 7, 4 (DK 59 A 111), qui associe Anaxagore et Parménide ; Curd, 2007, p. 174 et 226-227.</ref>. Owen Kember juge le témoignage d'Aristote inexact sur plusieurs points et reconstruit ainsi la théorie : les deux parents émettraient une semence venue de tout le corps, la part de chacun déterminerait la ressemblance, et le sexe dépendrait du côté de l'utérus où se dépose le mélange<ref>Owen Kember, « Anaxagoras' Theory of Sex Differentiation and Heredity », ''Phronesis'', vol. 18, 1973, p. 1-14, ici p. 1 et 14, contre l'interprétation reçue (Blersch, Erhard, Lesky, Lloyd), fondée sur Aristote.</ref>.
=== La génération des animaux et des plantes ===
Selon les témoignages, Anaxagore distinguait la première génération des animaux et leur reproduction ultérieure : les animaux naquirent d'abord de l'humide, du chaud et du terreux, puis les uns des autres<ref>Diogène Laërce, II, 9 (DK 59 A 1) ; Hippolyte, ''Réfutation'', I, 8, 12 (DK 59 A 42).</ref>. Irénée lui attribue l'idée que les animaux sont issus de semences tombées du ciel, et Théophraste celle que l'air contient les semences des plantes, entraînées vers le sol par la pluie<ref>Irénée, ''Contre les hérésies'', II, 14, 2 (DK 59 A 113) ; Théophraste, ''Recherches sur les plantes'', III, 1, 4 (DK 59 A 117) ; Schofield, 1980, p. 125.</ref>.
Plutarque attribue aux anaxagoréens, avec les platoniciens et les démocritéens, l'idée que la plante est un « animal attaché à la terre » ; le témoignage ne nomme donc pas directement Anaxagore comme auteur de cette formule<ref>Plutarque, ''Questions naturelles'', 1, 911d (DK 59 A 116) ; Curd, 2007, p. 125 et 175-176.</ref>. Le traité pseudo-aristotélicien ''Sur les plantes'' attribue en revanche directement à Anaxagore l'idée que les plantes éprouvent désir, plaisir et peine, et qu'elles possèdent intellect et connaissance<ref>Pseudo-Aristote, ''Sur les plantes'', I, 1, 815a15-20 et 815b16-17 (DK 59 A 117). Schofield (1980, p. 148, n. 40-41) juge difficile d'accorder crédit aux sentiments prêtés aux plantes, mais admet que l'attribution d'une âme et d'un intellect puisse reposer sur une source fiable.</ref>.
=== Observations biologiques diverses ===
Les sources conservent quelques observations isolées. Aristote rapporte qu'Anaxagore et d'autres physiciens prétendaient que le corbeau et l'ibis s'accouplent par le bec et que la belette met bas par la gueule, et il leur reproche d'en parler trop superficiellement et sans examen<ref>Aristote, ''Génération des animaux'', III, 6, 756b13-17 (DK 59 A 114).</ref>. Gershenson et Greenberg proposent d'expliquer l'erreur sur la belette par l'observation de femelles transportant leurs petits dans la gueule après la mise bas ; il s'agit de leur reconstruction, non d'un témoignage antique<ref>Gershenson et Greenberg, 1964, p. 56-57.</ref>. Anaxagore expliquait aussi que ce qu'on appelle proverbialement « lait d'oiseau » est le blanc de l'œuf<ref>Anaxagore, B22, cité par Athénée, II, 57d (DK 59 B 22) ; Tad Brennan, « The Text of Anaxagoras Fragment DK 59 B22 », ''American Journal of Philology'', vol. 116, 1995, p. 533-537.</ref>.
=== L'homme et les animaux ===
Selon Anaxagore, l'homme est le plus intelligent des animaux parce qu'il a des mains ; Aristote renverse la formule et soutient que l'homme a des mains parce qu'il est le plus intelligent<ref>Aristote, ''Parties des animaux'', IV, 10, 687a7-10 (DK 59 A 102) ; Schofield, 1980, p. 16.</ref>. Burnet en tirait l'idée que, l'intellect étant partout le même, les différences d'intelligence entre les êtres vivants dépendent de la structure de leur corps<ref>Burnet, 1930, chap. VI.</ref>.
== L'influence et la postérité ==
Anaxagore est le premier philosophe présocratique dont les sources attestent l'établissement à Athènes, où il contribue à introduire la tradition ionienne d'enquête sur la nature<ref>Curd, 2007, p. 129 et 142-146 ; Curd et Sisko, ''SEP'', 2026, § 1 et 6.</ref>. Pour sa postérité, il faut distinguer les filiations expressément attestées par les sources anciennes des rapprochements reconstruits par les historiens modernes.
=== La transmission immédiate : Archélaos, Diogène d'Apollonie et le papyrus de Derveni ===
La tradition fait d'Archélaos d'Athènes un disciple ou un successeur d'Anaxagore et, dans certaines sources, le maître de Socrate ; il est ainsi l'un des premiers relais auxquels la doxographie rattache la pensée anaxagoréenne<ref>Diogène Laërce, II, 16 (DK 60 A 1) ; Curd, 2007, p. 134 et n. 18. Ion de Chios rapporte que le jeune Socrate voyagea à Samos avec Archélaos (Diogène Laërce, II, 23 = DK 60 A 3).</ref>. Clément d'Alexandrie en fait le successeur d'Anaxagore, et Eusèbe précise qu'il lui succéda à la tête de l'école de Lampsaque, indication difficile à concilier avec les témoignages qui situent son enseignement à Athènes<ref>Clément d'Alexandrie, ''Stromates'', I, 63, et Eusèbe, ''Préparation évangélique'', X, 14, 13 (DK 59 A 7) ; Curd, 2007, p. 134, n. 18.</ref>. Archélaos semble avoir prolongé la cosmologie de son maître par un récit des origines des institutions sociales ; Gábor Betegh voit en lui l'un des premiers auteurs à avoir uni ces deux récits, innovation apparue à l'époque de Socrate<ref>Gábor Betegh, « Archelaus on Cosmogony and the Origins of Social Institutions », ''Oxford Studies in Ancient Philosophy'', vol. 51, 2016, p. 1-40.</ref>.
Diogène d'Apollonie, dans la génération suivante, subit l'influence d'Anaxagore dans sa doctrine comme dans son style ; il fait de l'intelligence un principe cosmique, mais l'attribue à l'air et prend soin de l'appuyer sur des arguments, ce qu'Anaxagore ne faisait guère<ref>Schofield, 1980, p. 5-6 ; Curd, « Presocratic Philosophy », ''SEP'', 2020, § 9.</ref>. L'auteur du papyrus de Derveni, commentaire allégorique d'un poème orphique découvert en 1962, semble avoir repris, en les modifiant, la doctrine d'un Intellect qui gouverne le cosmos et certains aspects de la théorie des ingrédients<ref>Curd, 2007, p. 143 et n. 35 ; Gábor Betegh, ''The Derveni Papyrus: Cosmology, Theology and Interpretation'', Cambridge, Cambridge University Press, 2004, chap. 7.</ref>.
=== L'influence sur Socrate : espoirs et déceptions ===
La relation entre Anaxagore et Socrate passe, dans nos sources, par la lecture du livre. Dans le ''Phédon'', Socrate raconte sa rencontre avec la pensée d'Anaxagore, qui suscita d'abord son enthousiasme, puis sa déception<ref>Platon, ''Phédon'', 97b-98c (DK 59 A 47).</ref>. Comme on l'a vu, cette déception porte sur une attente proprement socratique, celle d'une explication par le meilleur, qu'Anaxagore n'avait pas formulée dans les termes que Socrate aurait voulus.
Dans le récit du ''Phédon'', la déception provoquée par la lecture d'Anaxagore précède la recherche d'une autre méthode d'explication, présentée par Socrate comme une « seconde navigation »<ref>Platon, ''Phédon'', 97b-100a ; Curd, 2007, p. 144-145.</ref>. Ce lien appartient au récit philosophique de Platon ; il ne suffit pas, à lui seul, à établir une dépendance historique du Socrate réel à l'égard d'Anaxagore.
Dans l’''Apologie'', Mélétos attribue à Socrate des thèses cosmologiques que celui-ci renvoie explicitement aux livres d'Anaxagore ; Platon fait ainsi de la confusion entre Socrate et la philosophie naturelle anaxagoréenne un élément de la défense de Socrate<ref>Platon, ''Apologie de Socrate'', 26d-e (DK 59 A 35) ; Curd, 2007, p. 142-143.</ref>. Xénophon présente de son côté Socrate mettant ses disciples en garde contre l'étude des phénomènes célestes à la manière d'Anaxagore, qu'il accuse d'avoir perdu la raison en identifiant le soleil au feu<ref>Xénophon, ''Mémorables'', IV, 7, 6-7 (DK 59 A 73).</ref>.
=== L'appropriation platonicienne : du Noûs au Démiurge ===
Plusieurs interprètes ont rapproché le Noûs d'Anaxagore de la cosmologie téléologique du ''Timée''. Le Démiurge platonicien façonne le monde sensible en contemplant les Formes et en visant le meilleur ordre ; Curd et Schofield soulignent à la fois ce rapprochement et la distance qui sépare cette construction de ce qu'affirment les fragments d'Anaxagore<ref>Platon, ''Timée'', 29a-30c et 47e-48a ; Curd, 2007, p. 145 ; Schofield, 1980, p. 61 ; Glenn R. Morrow, « Necessity and Persuasion in Plato's ''Timaeus'' », ''Philosophical Review'', vol. 59, 1950, p. 147-163 ; Luc Brisson, ''Le Même et l'Autre dans la structure ontologique du Timée de Platon'', Paris, Klincksieck, 1974.</ref>. Schofield note notamment que, chez Anaxagore comme dans le ''Timée'', l'ordonnancement porte sur un donné matériel préexistant plutôt que sur une création à partir de rien<ref>Schofield, 1980, p. 61.</ref>.
Dans le ''Philèbe'', Socrate rappelle l'accord des sages pour dire que l'Intellect est roi du ciel et de la terre<ref>Platon, ''Philèbe'', 28c ; cf. ''Cratyle'', 413c (DK 59 A 55).</ref>. Le passage des ''Lois'' XII, 967b-c, où Platon évoque ceux qui ont affirmé que l'Intellect avait ordonné les réalités célestes, est couramment rapproché d'Anaxagore<ref>Platon, ''Lois'', XII, 967b-c ; sur les échos anaxagoréens chez Platon, voir Curd, 2007, p. 143-146.</ref>. Curd montre en outre que Platon réemploie plusieurs notions et formulations anaxagoréennes, notamment l'expression « lui-même par lui-même » et le vocabulaire de la participation, tout en transformant profondément leur fonction<ref>Curd, 2007, p. 50 et 143-145.</ref>.
=== La critique aristotélicienne ===
Aristote, tout en reconnaissant l'importance historique d'Anaxagore, en fut l'un des critiques les plus sévères. Dans la ''Métaphysique'', il dit qu'Anaxagore parut comme un homme sobre au milieu de prédécesseurs qui parlaient au hasard<ref>Aristote, ''Métaphysique'', A, 3, 984b15-20 (DK 59 A 58).</ref>, avant de lui reprocher de se servir de l'Intellect comme d'un ''deus ex machina''<ref>Aristote, ''Métaphysique'', A, 4, 985a18-21 (DK 59 A 47).</ref>. Ses critiques portent aussi, dans le traité ''De l'âme'', sur la confusion entre l'âme et l'intellect et sur la manière dont le Noûs connaît<ref>Aristote, ''De l'âme'', I, 2 et III, 4 (DK 59 A 55, A 99, A 100) ; Curd, 2007, p. 146.</ref>.
Aristote décrit par ailleurs la doctrine d'Anaxagore au moyen de son propre vocabulaire, notamment le terme d'homéomère (ὁμοιομερής), qu'Anaxagore n'a probablement jamais employé<ref>Curd, 2007, p. 147-150 ; Schofield, 1980, p. 128-132.</ref>. La distinction entre parties homéomères (chair, os, sang) et anhoméomères (main, visage) appartient à la biologie d'Aristote lui-même ; il s'en sert pour décrire rétrospectivement la doctrine d'Anaxagore<ref>Aristote, ''Parties des animaux'', II, 1, 646a12-24 ; ''Génération et corruption'', I, 1, 314a18-20 (DK 59 A 46).</ref>.
=== L'héritage dans la philosophie hellénistique et tardive ===
La réception stoïcienne d'Anaxagore fait aujourd'hui l'objet d'un débat plus précis. Les Stoïciens défendent une raison divine immanente et corporelle, ce qui les distingue du Noûs anaxagoréen tel que le décrivent de nombreux interprètes<ref>A. A. Long et D. N. Sedley, ''The Hellenistic Philosophers'', vol. I, Cambridge, Cambridge University Press, 1987 ; Michael J. White, « Stoic Natural Philosophy (Physics and Cosmology) », dans Brad Inwood (éd.), ''The Cambridge Companion to the Stoics'', Cambridge, Cambridge University Press, 2003, p. 124-152.</ref>. Christian Vassallo a toutefois soutenu récemment, à partir des sources relatives à l'ancien stoïcisme, que les premiers Stoïciens connaissaient le traité d'Anaxagore et lui devaient davantage qu'on ne l'avait généralement admis, notamment sur la continuité et la divisibilité infinie de la matière<ref>Christian Vassallo, « Anaxagoras Stoicus: Mind, Matter, and Cosmos », dans Christian Vassallo, Michele Alessandrelli et Stavros Kouloumentas (éd.), ''Stoic Presocratics - Presocratic Stoics: Studies in the Stoic Reception of Early Greek Philosophy'', Turnhout, Brepols, 2024, p. 269-300.</ref>.
Du côté épicurien, Lucrèce critique longuement l’''homoeomeria'' d'Anaxagore<ref>Lucrèce, ''De la nature'', I, 830-920 (DK 59 A 44) ; David Sedley, ''Lucretius and the Transformation of Greek Wisdom'', Cambridge, Cambridge University Press, 1998.</ref>, et Cicéron fait railler par l'épicurien Velléius son Intellect illimité<ref>Cicéron, ''De la nature des dieux'', I, 11, 26 (DK 59 A 48).</ref>. Selon Dioclès, cité par Diogène Laërce, Épicure approuvait pourtant Anaxagore plus que tout autre ancien, tout en s'en écartant sur certains points<ref>Diogène Laërce, X, 12 (DK 59 A 26).</ref>.
Dans la tradition néoplatonicienne, Anaxagore est lu à travers Platon et Aristote ; les commentateurs formulent ses principes du « tout en tout » et de la prédominance dans leur propre vocabulaire<ref>Proclus, ''Éléments de théologie'', 103 ; Schofield, 1980, p. 155, n. 52.</ref>. Simplicius, au VI{{e}} siècle, consacre de longs passages de son commentaire sur la ''Physique'' d'Aristote à Anaxagore : c'est à lui que nous devons la plupart des fragments<ref>Simplicius, ''Commentaire sur la Physique'', passim ; Teodorsson, 1982, p. 10.</ref>.
=== Échos littéraires ===
À l'époque moderne, Goethe met en scène Anaxagore face à Thalès dans la « Nuit de Walpurgis classique » du second ''Faust'' (acte II). Leur dispute sur la formation des roches et des montagnes, par le feu ou par l'eau, a longtemps été lue comme une parodie de la querelle du XVIII{{e}} siècle entre vulcanistes et neptunistes ; Kenneth Weisinger juge cette lecture insuffisante<ref>Kenneth D. Weisinger, « A Note on Homunculus, Thales, and Anaxagoras », ''Monatshefte'', vol. 64, 1972, p. 237-246, ici p. 237.</ref>.
=== Un héritage à ne pas surinterpréter ===
Anaxagore ne doit pas être assimilé à un atomiste : son refus d'un minimum de grandeur et son principe de mélange universel sont incompatibles avec les corps indivisibles et le vide de l'atomisme leucippéen et démocritéen<ref>Anaxagore, B3 et B6 ; Aristote, ''Génération et corruption'', I, 2, 315b28-317a2 et I, 8, 325a23-b5 ; Inwood, 1986 ; Curd, 2007, p. 181-187.</ref>. Les rapprochements avec la physique moderne doivent donc être présentés comme des analogies historiographiques et non comme des filiations, sauf lorsqu'une réception historique spécifique peut être documentée.
Il faut aussi se garder de faire rétrospectivement du Noûs un Dieu créateur : il ordonne un mélange qu'il ne crée pas et qu'il trouve déjà donné<ref>Schofield, 1980, p. 61 ; Curd, 2007, p. 61 et 201, n. 17.</ref>.
Les récits qui attribuent à Anaxagore des prédictions précises, notamment celles d'une éclipse ou de la chute de la pierre d'Aigos Potamos, sont tardifs et difficiles à concilier avec ce que nous savons de ses théories ; Curd et Burnet les traitent avec scepticisme, tandis que Graham cherche à reconstruire un sens plus général de la prétendue « prédiction » de la météorite<ref>Curd, 2007, p. 132 et n. 9-10 ; Burnet, 1930, chap. VI ; Daniel W. Graham, « Anaxagoras and the Comet », ''Ancient Philosophy'', vol. 33, 2013, p. 1-18.</ref>.
=== Bilan ===
La réception antique est marquée par un contraste bien attesté. Aristote loue l'introduction du Noûs comme principe de l'ordre cosmique, mais le Socrate du ''Phédon'' et Aristote reprochent à Anaxagore d'en faire un usage explicatif insuffisant<ref>Platon, ''Phédon'', 97b-98c (DK 59 A 47) ; Aristote, ''Métaphysique'', A, 3, 984b15-20 et A, 4, 985a18-21 (DK 59 A 58 et A 47) ; Curd, 2007, p. 142-146 et 204-205.</ref>.
Dans le ''Phédon'', la déception devant Anaxagore est suivie de la « seconde navigation » de Socrate et de son recours aux Formes ; dans le ''Timée'', Platon propose une cosmologie téléologique centrée sur le Démiurge ; Aristote, enfin, critique l'usage anaxagoréen du Noûs dans le cadre de sa propre théorie des causes<ref>Platon, ''Phédon'', 97b-100a ; ''Timée'', 29a-30c et 47e-48a ; Aristote, ''Métaphysique'', A, 3-7 ; Curd, 2007, p. 143-146 et 204-205 ; Schofield, 1980, p. 59-61.</ref>. Ces rapprochements documentent une réception et des transformations philosophiques ; ils ne permettent pas de réduire les doctrines de Platon ou d'Aristote à une simple filiation anaxagoréenne.
Les témoignages attribuent à Anaxagore des explications naturalistes du soleil comme corps incandescent, de la lune comme corps terreux éclairé par le soleil, et du tonnerre et de l'éclair par des processus physiques dans l'atmosphère<ref>Hippolyte, ''Réfutation'', I, 8, 6-11 (DK 59 A 42) ; Aristote, ''Météorologiques'', II, 9, 369b14-19 (DK 59 A 84) ; Curd, 2007, p. 206-225.</ref>. Gershenson et Greenberg ont interprété l'ensemble de ces démarches comme les éléments d'une méthode scientifique proche de la conception moderne ; cette thèse forte demeure une interprétation historiographique, non une donnée des fragments ou des témoignages<ref>Gershenson et Greenberg, 1964, préface et p. 58-59 ; G. E. R. Lloyd, ''Early Greek Science: Thales to Aristotle'', Londres, Chatto & Windus, 1970.</ref>.
Les sources et les études modernes permettent ainsi de situer Anaxagore à la jonction de l'enquête ionienne sur la nature et de la philosophie athénienne du V{{e}} siècle, tout en distinguant sa doctrine propre de ses réélaborations ultérieures par Platon, Aristote et d'autres traditions<ref>Curd, 2007, p. 129-146 et 206-234 ; Schofield, 1980 ; Curd et Sisko, ''SEP'', 2026, § 1, 5 et 6.</ref>.
== Notes et références ==
{{references|colonnes=2}}
== Bibliographie ==
=== Textes anciens : éditions et traductions ===
; Diels, Hermann & Kranz, Walther (éd.)
: ''Die Fragmente der Vorsokratiker'', 6{{e}} éd., Berlin, Weidmann, 1951-1952, 3 vol.
: [Anaxagore : vol. II, p. 5-44, chap. 59, numérotation DK]
; Laks, André & Most, Glenn W. (éd.)
: ''Early Greek Philosophy'', Cambridge (Mass.), Harvard University Press, 2016, 9 vol. (Loeb Classical Library)
: [Anaxagore : chap. 25, numérotation LM]
; Laks, André & Most, Glenn W. (éd.)
: ''Les Débuts de la philosophie. Des premiers penseurs grecs à Socrate'', Paris, Fayard, 2016
; Curd, Patricia (éd.)
: ''Anaxagoras of Clazomenae: Fragments and Testimonia. A Text and Translation with Notes and Essays'', Toronto, University of Toronto Press, 2007 (Phoenix Presocratics, 6 ; Phoenix Supplementary Volume, 44)
; Sider, David (éd.)
: ''The Fragments of Anaxagoras, Edited with an Introduction and Commentary'', Meisenheim am Glan, Hain, 1981 (Beiträge zur klassischen Philologie, 118) ; 2{{e}} éd. revue, Sankt Augustin, Academia Verlag, 2005 (International Pre-Platonic Studies)
; Zafiropulo, Jean
: ''Anaxagore de Clazomène'', I : ''Le Mythe grec traditionnel de Thalès à Platon'' ; II : ''Théorie et fragments'', Paris, Les Belles Lettres, 1948 (Collection d'études anciennes)
; Kirk, G. S., Raven, J. E. & Schofield, Malcolm
: ''The Presocratic Philosophers: A Critical History with a Selection of Texts'', 2{{e}} éd., Cambridge, Cambridge University Press, 1983
; Simplicius
: ''In Aristotelis Physicorum libros commentaria'', éd. Hermann Diels, Berlin, Reimer, 1882-1895 (Commentaria in Aristotelem Graeca, 9-10)
; Platon
: ''Phédon'', trad. Monique Dixsaut, Paris, Flammarion, 1991 (GF Flammarion)
; Platon
: ''Apologie de Socrate. Criton'', trad. Luc Brisson, Paris, Flammarion, 1997 (GF Flammarion)
; Platon
: ''Timée. Critias'', trad. Luc Brisson, Paris, Flammarion, 1992 (GF Flammarion)
; Platon
: ''Philèbe'', texte établi et traduit par Auguste Diès, Paris, Les Belles Lettres, 1941 (Collection des universités de France)
; Platon
: ''Les Lois'', texte établi et traduit par Édouard des Places (livres I-VI, 1951) et Auguste Diès (livres VII-XII, 1956), Paris, Les Belles Lettres (Collection des universités de France)
; Aristote
: ''La Métaphysique'', trad. Jean Tricot, Paris, Vrin, 1933 (nouvelle éd. 1953)
; Aristote
: ''Physique'', trad. Pierre Pellegrin, Paris, Flammarion, 2000 (GF Flammarion)
; Aristote
: ''Traité du ciel'', trad. Jean Tricot, Paris, Vrin, 1949
; Aristote
: ''De la génération et de la corruption'', trad. Jean Tricot, Paris, Vrin, 1934
; Aristote
: ''Les Météorologiques'', trad. Jean Tricot, Paris, Vrin, 1941
; Aristote
: ''De la génération des animaux'', texte établi et traduit par Pierre Louis, Paris, Les Belles Lettres, 1961 (Collection des universités de France)
; Aristote
: ''Les Parties des animaux'', texte établi et traduit par Pierre Louis, Paris, Les Belles Lettres, 1956 (Collection des universités de France)
; Aristote
: ''Histoire des animaux'', trad. Jean Tricot, Paris, Vrin, 1957
; Aristote
: ''De l'âme'', trad. Richard Bodéüs, Paris, Flammarion, 1993 (GF Flammarion)
; Diogène Laërce
: ''Vies et doctrines des philosophes illustres'', sous la dir. de Marie-Odile Goulet-Cazé, Paris, Le Livre de Poche, 1999 (La Pochothèque)
; Théophraste
: ''De sensibus'', éd. et trad. angl. dans George M. Stratton, ''Theophrastus and the Greek Physiological Psychology before Aristotle'', Londres, Allen & Unwin, 1917
; Lucrèce
: ''De la nature'', texte établi et traduit par Alfred Ernout, Paris, Les Belles Lettres, 1920 (Collection des universités de France)
; Cicéron
: ''La Nature des dieux'', trad. Clara Auvray-Assayas, Paris, Les Belles Lettres, 2002 (La Roue à livres)
; Plutarque
: ''Vies parallèles'', trad. Anne-Marie Ozanam, éd. François Hartog, Paris, Gallimard, 2001 (Quarto)
; Xénophon
: ''Mémorables'', texte établi et traduit par Louis-André Dorion et Michele Bandini, Paris, Les Belles Lettres, 2000-2011 (Collection des universités de France)
=== Études sur Anaxagore ===
; Althoff, Jochen
: « Presocratic Discourse in Poetry and Prose: The Case of Empedocles and Anaxagoras », ''Studies in History and Philosophy of Science'', vol. 43, 2012, p. 293-299
; Arsenijević, Miloš, Popović, Saša & Vuletić, Miloš
: « Anaxagoras, the Thoroughgoing Infinitist: The Relation between his Teachings on Multitude and on Heterogeneity », ''European Journal of Analytic Philosophy'', vol. 15, 2019, p. 35-70
; Bargrave-Weaver, D.
: « The Cosmogony of Anaxagoras », ''Phronesis'', vol. 4, 1959, p. 77-91
; Barnes, Jonathan
: ''The Presocratic Philosophers'', Londres, Routledge, 1979, 2 vol. ; éd. révisée en un volume, 1982
; Betegh, Gábor
: « Archelaus on Cosmogony and the Origins of Social Institutions », ''Oxford Studies in Ancient Philosophy'', vol. 51, 2016, p. 1-40
; Bicknell, P. J.
: « Did Anaxagoras Observe a Sunspot in 467 B.C.? », ''Isis'', vol. 59, 1968, p. 87-90
; Brennan, Tad
: « The Text of Anaxagoras Fragment DK 59 B22 », ''American Journal of Philology'', vol. 116, 1995, p. 533-537
; Bröcker, Walter
: « Die Lehre des Anaxagoras », ''Kant-Studien'', vol. 42, 1942-1943, p. 176-189
; Burnet, John
: ''Early Greek Philosophy'', 4{{e}} éd., Londres, A. & C. Black, 1930
: [Le chapitre VI reprend celui de la 3{{e}} éd., 1920]
; Cleve, Felix M.
: ''The Philosophy of Anaxagoras: An Attempt at Reconstruction'', New York, King's Crown Press, 1949 ; nouvelle éd., ''The Philosophy of Anaxagoras, as Reconstructed by Felix M. Cleve'', La Haye, Martinus Nijhoff, 1973
; Cornford, F. M.
: « Anaxagoras' Theory of Matter », ''Classical Quarterly'', vol. 24, 1930, p. 14-30 et 83-95
; Couprie, Dirk L.
: « Anaxagoras on the Milky Way and Lunar Eclipses », ''Hyperboreus'', vol. 23, 2017, p. 181-207
; Couprie, Dirk L.
: « Anaxagoras on the Light and Phases of the Moon », ''Hyperboreus'', vol. 24, 2018, p. 12-39
; Curd, Patricia
: ''The Legacy of Parmenides: Eleatic Monism and Later Presocratic Thought'', Princeton, Princeton University Press, 1998 ; rééd. Las Vegas, Parmenides Publishing, 2004
; Curd, Patricia & Sisko, John
: « Anaxagoras », ''Stanford Encyclopedia of Philosophy'', 1{{re}} publication 2007, révision substantielle 2026
; Davison, J. A.
: « Protagoras, Democritus, and Anaxagoras », ''Classical Quarterly'', n.s. vol. 3, 1953, p. 33-45
; DeFilippo, Joseph G.
: « Reply to André Laks on Anaxagoras' ΝΟΥΣ », ''Southern Journal of Philosophy'', vol. 31, suppl., 1993, p. 39-48
; Deichgräber, Karl
: « Hymnische Elemente in der philosophischen Prosa der Vorsokratiker », ''Philologus'', vol. 88, 1933, p. 347-361
; Dover, K. J.
: « The Freedom of the Intellectual in Greek Society », ''Talanta'', vol. 7, 1975, p. 24-54
; Drozdek, Adam
: « Anaxagoras and the Everything in Everything Principle », ''Hermes'', vol. 133, 2005, p. 163-177
; Erhard, Hubert
: « Anaxagoras als Biologe », ''Sudhoffs Archiv für Geschichte der Medizin und der Naturwissenschaften'', vol. 35, 1942, p. 117-140
; Freeman, Kathleen
: « Anaxagoras », ''Greece & Rome'', vol. 4, 1935, p. 65-75
; Fritz, Kurt von
: « Der ΝΟΥΣ des Anaxagoras », ''Archiv für Begriffsgeschichte'', vol. 9, 1964, p. 87-102 ; repris dans ''Grundprobleme der Geschichte der antiken Wissenschaft'', Berlin et New York, De Gruyter, 1971, p. 576-593
; Furley, David J.
: « Anaxagoras in Response to Parmenides », ''Canadian Journal of Philosophy'', suppl. vol. 2, 1976, p. 61-85 ; repris dans ''Cosmic Problems'', Cambridge, Cambridge University Press, 1989, p. 47-65
; Furley, David J.
: « Anaxagoras, Plato and the Naming of Parts », dans Victor Caston et Daniel W. Graham (éd.), ''Presocratic Philosophy: Essays in Honour of Alexander Mourelatos'', Aldershot, Ashgate, 2002, p. 119-126
; Furth, Montgomery
: « A “Philosophical Hero”? Anaxagoras and the Eleatics », ''Oxford Studies in Ancient Philosophy'', vol. 9, 1991, p. 95-129
; Geffcken, Johannes
: « Die Asebeia des Anaxagoras », ''Hermes'', vol. 42, 1907, p. 127-133
; Gershenson, Daniel E. & Greenberg, Daniel A.
: ''Anaxagoras and the Birth of Scientific Method'', New York et Londres, Blaisdell, 1964 (A Blaisdell Book in the History of Science)
: [Version abrégée de la première partie de l'ouvrage suivant ; à utiliser avec précaution : la thèse d'une « naissance de la méthode scientifique » doit être nuancée]
; Gershenson, Daniel E. & Greenberg, Daniel A.
: ''Anaxagoras and the Birth of Physics'', New York et Londres, Blaisdell, 1964, xxv-538 p. (A History of Physics, série I)
: [La préface de l'édition abrégée l'annonce pour 1963 ; Sider (1974) et le compte rendu de ''Nature'' (1964) donnent 1964]
; Gigon, Olof
: « Zu Anaxagoras », ''Philologus'', vol. 91, 1936-1937, p. 1-41 ; repris dans ''Studien zur antiken Philosophie'', Berlin et New York, De Gruyter, 1972
; Graham, Daniel W.
: « The Postulates of Anaxagoras », ''Apeiron'', vol. 27, 1994, p. 77-121
; Graham, Daniel W.
: « Empedocles and Anaxagoras: Responses to Parmenides », dans A. A. Long (éd.), ''The Cambridge Companion to Early Greek Philosophy'', Cambridge, Cambridge University Press, 1999, p. 159-180
; Graham, Daniel W.
: « Was Anaxagoras a Reductionist? », ''Ancient Philosophy'', vol. 24, 2004, p. 1-18
; Graham, Daniel W.
: « Anaxagoras and the Comet », ''Ancient Philosophy'', vol. 33, 2013, p. 1-18
; Graham, Daniel W. & Hintz, Eric
: « Anaxagoras and the Solar Eclipse of 478 BC », ''Apeiron'', vol. 40, 2007, p. 319-344
; Inwood, Brad
: « Anaxagoras and Infinite Divisibility », ''Illinois Classical Studies'', vol. 11, 1986, p. 17-33
; Janko, Richard
: « Eclipse and Plague: Themistocles, Pericles, Anaxagoras and the Athenians' War on Science », ''Journal of Hellenic Studies'', vol. 140, 2020, p. 213-237
; Kember, Owen
: « Anaxagoras' Theory of Sex Differentiation and Heredity », ''Phronesis'', vol. 18, 1973, p. 1-14
; Kerferd, George B.
: « Anaxagoras and the Concept of Matter before Aristotle », ''Bulletin of the John Rylands Library'', vol. 52, 1969, p. 129-143 ; repris dans Mourelatos (éd.), 1974, p. 489-503
; Kingsley, Peter
: « Notes on Air: Four Questions of Meaning in Empedocles and Anaxagoras », ''Classical Quarterly'', n.s. vol. 45, 1995, p. 26-29
; Laks, André
: « Mind's Crisis: On Anaxagoras' ''Nous'' », ''Southern Journal of Philosophy'', vol. 31, suppl., 1993, p. 19-38
; Lesher, James H.
: « Mind's Knowledge and Powers of Control in Anaxagoras DK B12 », ''Phronesis'', vol. 40, 1995, p. 125-142
; Lewis, Eric
: « Anaxagoras and the Seeds of a Physical Theory », ''Apeiron'', vol. 33, 2000, p. 1-23
; Mann, William E.
: « Anaxagoras and the ''Homoiomerē'' », ''Phronesis'', vol. 25, 1980, p. 228-249
; Mansfeld, Jaap
: « The Chronology of Anaxagoras' Athenian Period and the Date of His Trial », I, ''Mnemosyne'', vol. 32, 1979, p. 39-69 ; II, ''Mnemosyne'', vol. 33, 1980, p. 17-95
; Mansfeld, Jaap
: « Anaxagoras' Other World », ''Phronesis'', vol. 25, 1980, p. 1-4
; Marmodoro, Anna
: « Anaxagoras's Qualitative Gunk », ''British Journal for the History of Philosophy'', vol. 23, 2015, p. 402-422
; Marmodoro, Anna
: ''Everything in Everything: Anaxagoras's Metaphysics'', New York, Oxford University Press, 2017
; O'Brien, Denis
: « The Relation of Anaxagoras and Empedocles », ''Journal of Hellenic Studies'', vol. 88, 1968, p. 93-113
; O'Brien, Denis
: « Derived Light and Eclipses in the Fifth Century », ''Journal of Hellenic Studies'', vol. 88, 1968, p. 114-127
; Paxson, Thomas D., Jr.
: « The Holism of Anaxagoras », ''Apeiron'', vol. 17, 1983, p. 85-91
; Peck, Arthur L.
: « Anaxagoras and the Parts », ''Classical Quarterly'', vol. 20, 1926, p. 57-71
; Peck, Arthur L.
: « Anaxagoras: Predication as a Problem in Physics », ''Classical Quarterly'', vol. 25, 1931, p. 27-37 et 112-120
; Pinto, Rhodes
: « ''Nous'', Motion, and Teleology in Anaxagoras », ''Oxford Studies in Ancient Philosophy'', vol. 52, 2017, p. 1-32
; Potts, Ronald
: « Anaxagoras' Cosmogony », ''Apeiron'', vol. 18, 1984, p. 90-96
; Raven, J. E.
: « The Basis of Anaxagoras' Cosmology », ''Classical Quarterly'', n.s. vol. 4, 1954, p. 123-137
; Reesor, Margaret E.
: « The Meaning of Anaxagoras », ''Classical Philology'', vol. 55, 1960, p. 1-8
; Rösler, Wolfgang
: « ΟΜΟΥ ΧΡΗΜΑΤΑ ΠΑΝΤΑ ΗΝ », ''Hermes'', vol. 99, 1971, p. 246-248
; Schofield, Malcolm
: « Doxographica Anaxagorea », ''Hermes'', vol. 103, 1975, p. 1-24
; Schofield, Malcolm
: ''An Essay on Anaxagoras'', Cambridge, Cambridge University Press, 1980 (Cambridge Classical Studies)
; Sider, David
: « Anaxagoras on the Size of the Sun », ''Classical Philology'', vol. 68, 1973, p. 128-129
; Sider, David
: « A Note on Anaxagoras, Fr. 1 », ''Archiv für Geschichte der Philosophie'', vol. 55, 1973, p. 249-251
; Sider, David
: « Anaxagoras Fr. 14 DK », ''Hermes'', vol. 102, 1974, p. 365-367
; Sisko, John E.
: « Anaxagoras' Parmenidean Cosmology: Worlds within Worlds within the One », ''Apeiron'', vol. 36, 2003, p. 87-114
; Sisko, John E.
: « Anaxagoras betwixt Parmenides and Plato », ''Philosophy Compass'', vol. 5, 2010, p. 432-442
; Sisko, John E.
: « Anaxagoras on Matter, Motion, and Multiple Worlds », ''Philosophy Compass'', vol. 5, 2010, p. 443-454
; Solmsen, Friedrich
: « Anaxagoras B 19 Diels-Kranz », ''Hermes'', vol. 91, 1963, p. 251-252
; Stokes, M. C.
: « On Anaxagoras. Part I: Anaxagoras' Theory of Matter », ''Archiv für Geschichte der Philosophie'', vol. 47, 1965, p. 1-19 ; « Part II: The Order of Cosmogony », ibid., p. 217-250
; Strang, Colin
: « The Physical Theory of Anaxagoras », ''Archiv für Geschichte der Philosophie'', vol. 45, 1963, p. 101-118 ; repris dans Furley et Allen (éd.), vol. II, 1975, p. 361-380
; Tannery, Paul
: « La théorie de la matière d'Anaxagore », ''Revue philosophique'', vol. 22, 1886, p. 255-274 ; repris dans ''Pour l'histoire de la science hellène'', Paris, Alcan, 1887
; Taylor, A. E.
: « On the Date of the Trial of Anaxagoras », ''Classical Quarterly'', vol. 11, 1917, p. 81-87
; Teodorsson, Sven-Tage
: ''Anaxagoras' Theory of Matter'', Göteborg, Acta Universitatis Gothoburgensis, 1982 (Studia Graeca et Latina Gothoburgensia, 42)
; Theodossiou, Evangelos Th., Niarchos, P. G., Manimanis, V. N. & Orchiston, Wayne
: « The Fall of a Meteorite at Aegos Potami in 467/466 BC », ''Journal of Astronomical History and Heritage'', vol. 5, 2002, p. 135-140
; Vlastos, Gregory
: « The Physical Theory of Anaxagoras », ''Philosophical Review'', vol. 59, 1950, p. 31-57 ; repris dans Furley et Allen (éd.), vol. II, 1975, p. 323-353, et dans ''Studies in Greek Philosophy'', vol. I, éd. Daniel W. Graham, Princeton, Princeton University Press, 1995, p. 303-327
; Warren, James
: « Anaxagoras on Perception, Pleasure, and Pain », ''Oxford Studies in Ancient Philosophy'', vol. 33, 2007, p. 19-54
; Wasserstein, A.
: « A Note on Fragment 12 of Anaxagoras », ''Classical Review'', n.s. vol. 10, 1960, p. 4-5
; Weisinger, Kenneth D.
: « A Note on Homunculus, Thales, and Anaxagoras », ''Monatshefte'', vol. 64, 1972, p. 237-246
; Woodbury, Leonard
: « Anaxagoras and Athens », ''Phoenix'', vol. 35, 1981, p. 295-315
=== Comptes rendus ===
; Hall, Marie Boas
: « Anaxagoras Revisited », compte rendu de Gershenson et Greenberg, 1964, ''Nature'', vol. 204, 1964 (supplément du 5 décembre), p. 968
; Louguet, Claire
: Compte rendu de Curd, ''Anaxagoras of Clazomenae'', 2007, ''Classical Review'', n.s. vol. 59, 2009, p. 23-24
; Naddaf, Gerard
: Compte rendu de Graham, ''Science before Socrates'', 2013, ''Mind'', vol. 125, 2016, p. 945-952
; Rawlins, F. I. G.
: « Anaxagoras: Philosopher and Artist », compte rendu de Cleve, 1949, ''Nature'', vol. 166, 1950, p. 2-3
; Sider, David
: Compte rendu de Teodorsson, ''Anaxagoras' Theory of Matter'', 1982, ''Classical Journal'', vol. 80, 1984, p. 71-73
=== Présocratiques, sciences et contexte ===
; Betegh, Gábor
: ''The Derveni Papyrus: Cosmology, Theology and Interpretation'', Cambridge, Cambridge University Press, 2004
; Curd, Patricia
: « Presocratic Philosophy », ''Stanford Encyclopedia of Philosophy'', 1{{re}} publication 2007, révision substantielle 2020
; Conway, Declan
: « The Climate and Hydrology of the Upper Blue Nile River », ''The Geographical Journal'', vol. 166, no 1, 2000, p. 49-62
; Dover, Kenneth
: ''Aristophanes: Clouds'', éd. avec introduction et commentaire, Oxford, Clarendon Press, 1968
; Fränkel, Hermann
: ''Wege und Formen frühgriechischen Denkens'', 2{{e}} éd., Munich, Beck, 1960
; Furley, David J. & Allen, R. E. (éd.)
: ''Studies in Presocratic Philosophy'', Londres, Routledge and Kegan Paul, 1970-1975, 2 vol.
; Graham, Daniel W.
: « La lumière de la lune dans la pensée grecque archaïque », dans André Laks et Claire Louguet (éd.), ''Qu'est-ce que la philosophie présocratique ?'', Lille, Presses universitaires du Septentrion, 2002, p. 351-380
; Graham, Daniel W.
: ''Explaining the Cosmos: The Ionian Tradition of Scientific Philosophy'', Princeton, Princeton University Press, 2006
; Graham, Daniel W.
: ''Science before Socrates: Parmenides, Anaxagoras, and the New Astronomy'', New York, Oxford University Press, 2013
; Guthrie, W. K. C.
: ''A History of Greek Philosophy'', vol. II : ''The Presocratic Tradition from Parmenides to Democritus'', Cambridge, Cambridge University Press, 1965
; Lloyd, G. E. R.
: ''Polarity and Analogy: Two Types of Argumentation in Early Greek Thought'', Cambridge, Cambridge University Press, 1966
; Lloyd, G. E. R.
: ''Early Greek Science: Thales to Aristotle'', Londres, Chatto & Windus, 1970
; Mourelatos, A. P. D. (éd.)
: ''The Pre-Socratics: A Collection of Critical Essays'', Garden City, Anchor Press, 1974
; Norden, Eduard
: ''Agnostos Theos. Untersuchungen zur Formengeschichte religiöser Rede'', Leipzig, Teubner, 1913 (4{{e}} tirage, Stuttgart, 1956)
; Owen, G. E. L.
: « Eleatic Questions », ''Classical Quarterly'', n.s. vol. 10, 1960, p. 84-102
; Owen, G. E. L.
: « Plato and Parmenides on the Timeless Present », ''The Monist'', vol. 50, 1966, p. 317-340 ; repris dans Mourelatos (éd.), 1974, p. 271-292
; Sedley, David
: ''Creationism and Its Critics in Antiquity'', Berkeley, University of California Press, 2007
; Vassallo, Christian
: ''The Presocratics at Herculaneum: A Study of Early Greek Philosophy in the Epicurean Tradition'', Berlin et Boston, De Gruyter, 2021 (Studia Praesocratica, 11)
=== Influence et réception ===
; Brisson, Luc
: ''Le Même et l'Autre dans la structure ontologique du Timée de Platon'', Paris, Klincksieck, 1974
; Long, A. A. & Sedley, D. N.
: ''The Hellenistic Philosophers'', vol. I, Cambridge, Cambridge University Press, 1987
; Morrow, Glenn R.
: « Necessity and Persuasion in Plato's ''Timaeus'' », ''Philosophical Review'', vol. 59, 1950, p. 147-163
; Sedley, David
: ''Lucretius and the Transformation of Greek Wisdom'', Cambridge, Cambridge University Press, 1998
; Vassallo, Christian
: « Anaxagoras Stoicus: Mind, Matter, and Cosmos », dans Christian Vassallo, Michele Alessandrelli et Stavros Kouloumentas (éd.), ''Stoic Presocratics - Presocratic Stoics: Studies in the Stoic Reception of Early Greek Philosophy'', Turnhout, Brepols, 2024, p. 269-300
; White, Michael J.
: « Stoic Natural Philosophy (Physics and Cosmology) », dans Brad Inwood (éd.), ''The Cambridge Companion to the Stoics'', Cambridge, Cambridge University Press, 2003, p. 124-152
=== Dictionnaires et encyclopédies ===
; Goulet, Richard (dir.)
: ''Dictionnaire des philosophes antiques'', vol. I, Paris, CNRS Éditions, 1989
{{autocat}}
[[Catégorie:Philosophe]]
{{DEFAULTSORT:Anaxagore}}
[[Catégorie:Présocratiques]]
qfmbis6tvc67jl6z3seen7vk7rt4xzj
Mathc initiation/0077
0
84523
773112
773104
2026-09-25T11:59:06Z
Xhungab
23827
773112
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc initiation (livre)]]
[[Mathc initiation/0075#* Résoudre : y(n) - 2 y(n-1) = δ(n)|Sommaire]]
'''Résoudre l'équation aux différences :'''
y[n] - 2 y[n-1] = 3^(n) u[n]
et une valeur initiale y[-1] = 0 (n = 0; y[n-1] = y[0-1] = y[-1])
on veut retrouver y[n]
'''Utilisons la transformée en z''' :
x[n] <--> X(z)
y[n] - 2 y[n-1] = 3^(n) u[n] x[n-1] <--> z^(-1) X(z)
3^(n) u[n] <--> z/(z-3)
Y(z) - 2 z^(-1) Y(z) = z/z-3 Isolons Y(z)
Y(z) (1-2 z^(-1)) = z/z-3
Y(z) = (z/z-3)/(1-2 z^(-1))
Y(z) = z /(z-3) (1-2 z^(-1)) Multiplions par z/z
Y(z) = z^2/(z-3) (z-2 )
Utilisons les fractions partielles
z^2 Az Bz
------------ = ----- + ----- Az car nous voulons une solution
(z-3) (z-2 ) (z-3) (z-2) de la forme Az/(z-a) et non A/(z-a)
z = A(z-2) + B(z-3)
si z = 3 A = 3
si z = 2 B = -2
donc
z^2 z z
------------ = 3 ----- - 2 -----
(z-3) (z-2 ) (z-3) (z-2)
Conclusion : y[n] = 3 3^(n)u[n] - 2 2^(n)u[n] (z/(z-a)<-->[[Mathc initiation/a585|a^n u[n]]])
Soit y[n] = 3^(n+1)u[n] - 2^(n+1)u[n]
{{AutoCat}}
f8ojhm4c3oc602gwifpmbfb6dpk7c3o
Mathc initiation/0078
0
84524
773113
773108
2026-09-25T11:59:30Z
Xhungab
23827
773113
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc initiation (livre)]]
[[Mathc initiation/0075#* Résoudre : y(n) - 2 y(n-1) = δ(n)|Sommaire]]
'''Résoudre l'équation aux différences :'''
y[n] - 2 y[n-1] = 3^(n) u[n]
et une valeur initiale y[-1] = 1 (n = 0; y[n-1] = y[0-1] = y[-1])
on veut retrouver y[n]
'''Utilisons la transformée en z''' :
x[n] <--> X(z)
y[n] - 2 y[n-1] = 3^(n) u[n] x[n-1] <--> z^(-1)X(z)+x[-1] (si x[-1]!=0)
3^(n) u[n] <--> z/(z-3)
Y(z) - 2(z^(-1) Y(z)+'''y[-1]''') = z/z-3
Y(z) - 2(z^(-1) Y(z)'''+1''') = z/z-3
Y(z) - 2 z^(-1) Y(z)'''-2''' = z/z-3 Développons par 2
Y(z) - 2 z^(-1) Y(z) = z/(z-3) '''+2''' Isolons Y(z)
Y(z) (1-2 z^(-1)) = z/(z-3) +2
... = z/(z-3) +2 (z-3)/(z-3)
... = (z +2 (z-3))/(z-3)
... = (z +2 z-6)/(z-3)
... = (3 z-6)/(z-3)
... = 3 (z-2)/(z-3)
Y(z) '''(1-2 z^(-1))''' = --- Multiplions par z/z
Y(z) '''(z-2/z)''' = ---
Y(z) '''(z-2/z)''' = 3 (z-2)/(z-3) Isolons Y(z)
Y(z) = (3 (z-2)/(z-3)) '''/ (z-2/z)'''
Y(z) = '''z''' 3 (z-2)/(z-3)'''(z-2)'''
Y(z) = 3 z/(z-3) Simplifions par (z-2)
Conclusion : y[n] = 3 3^(n)u[n] (z/(z-a)<-->[[Mathc initiation/a585|a^n u[n]]])
'''Soit y[n] = 3^(n+1)u[n]'''
{{AutoCat}}
p5zgbno88wuw49ids1rwvr6zlnt07qu
773114
773113
2026-09-25T12:00:58Z
Xhungab
23827
773114
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc initiation (livre)]]
[[Mathc initiation/0075#* Résoudre : y(n) - 2 y(n-1) = δ(n)|Sommaire]]
'''Résoudre l'équation aux différences :'''
y[n] - 2 y[n-1] = 3^(n) u[n]
et une valeur initiale y[-1] = 1 (n = 0; y[n-1] = y[0-1] = y[-1])
on veut retrouver y[n]
'''Utilisons la transformée en z''' :
x[n] <--> X(z)
y[n] - 2 y[n-1] = 3^(n) u[n] x[n-1] <--> z^(-1)X(z)+x[-1] (si x[-1]!=0)
3^(n) u[n] <--> z/(z-3)
Y(z) - 2(z^(-1) Y(z)+'''y[-1]''') = z/z-3
Y(z) - 2(z^(-1) Y(z)'''+1''') = z/z-3
Y(z) - 2 z^(-1) Y(z)'''-2''' = z/z-3 Développons par 2
Y(z) - 2 z^(-1) Y(z) = z/(z-3) '''+2''' Isolons Y(z)
Y(z) (1-2 z^(-1)) = z/(z-3) +2
... = z/(z-3) +2 (z-3)/(z-3)
... = (z +2 (z-3))/(z-3)
... = (z +2 z-6)/(z-3)
... = (3 z-6)/(z-3)
... = 3 (z-2)/(z-3)
Y(z) '''(1-2 z^(-1))''' = --- Multiplions par z/z
Y(z) '''(z-2/z)''' = ---
Y(z) '''(z-2/z)''' = 3 (z-2)/(z-3) Isolons Y(z)
Y(z) = (3 (z-2)/(z-3)) '''/ (z-2/z)'''
Y(z) = '''z''' 3 (z-2)/(z-3)'''(z-2)''' Simplifions par (z-2)
Y(z) = 3 z/(z-3)
Conclusion : y[n] = 3 3^(n)u[n] (z/(z-a)<-->[[Mathc initiation/a585|a^n u[n]]])
'''Soit y[n] = 3^(n+1)u[n]'''
{{AutoCat}}
a1s4e1hqpnpo7ivds30ykih1qvkjh2p
Mathc initiation/0079
0
84525
773223
2026-09-26T09:54:46Z
Xhungab
23827
news
773223
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc initiation (livre)]]
[[Mathc initiation/0075|Sommaire]]
* [[#Le retard de no unités : Z(x(n-no)) = z^(-no) X(z)| '''Le retard de no unités : Z(x[n-no]) = z^(-no) X(z)''']]
* [[#Multiplication par une exponentielle : a^n x(n)| '''Multiplication par une exponentielle : a^n x[n]''']]
* [[#Multiplication par la variable d'évolution : Z(n x(n)) = -z X'(z)| '''Multiplication par la variable d'évolution : Z(n x[n]) = -z X'(z)''']]
* [[#Multiplication par la variable d'évolution n^2 : Z(n^2 x(n)) = -z X"(z)| '''Multiplication par la variable d'évolution n^2 : Z(n^2 x[n]) = -z X"(z)''']]
==Le retard de no unités : Z(x(n-no)) = z^(-no) X(z)==
Donnez la transformée en z de cette équation :
y[n]
- 3 y[n-1]
+ 5 y[n-2]
- 7 y[n-3] = δ[n]
Les valeurs initiales y[-1] = 2; y[-2] = 4; y[-3] = 6;
Y(z)
- 3 (z^(-1) Y(z) + y[-1])
+ 5 (z^(-2) Y(z) + z^(-1) y[-1] + y[-2])
- 7 (z^(-3) Y(z) + z^(-2) y[-1] + z^(-1)y[-2] + y[-3]) = 1
==Multiplication par une exponentielle : a^n x(n)==
Donnez la transformée en z de cette équation :
y[n] =
- 3^(n) u[n]
+ 2 5^(n) u[n]
+ 4 7^(n) u[n]
Y(z) =
- z/(z-3)
+ 2 z/(z-5)
+ 4 z/(z-7)
==Multiplication par la variable d'évolution : Z(n x(n)) = -z X'(z)==
Donnez la transformée en z de cette équation :
y[n] =
- n 3^(n) u[n]
+ 2 n 5^(n) u[n]
+ 4 n 7^(n) u[n]
Y(z) =
- 3 z/(z-3)^2
+ 2 5 z/(z-5)^2
+ 4 7 z/(z-7)^2
==Multiplication par la variable d'évolution n^2 : Z(n^2 x(n)) = -z X"(z)==
Donnez la transformée en z de cette équation :
y[n] =
- n^2 3^(n) u[n]
+ 2 n^2 5^(n) u[n]
+ 4 n^2 7^(n) u[n]
Y(z) =
- 3 z (z+3)/(z-3)^3
+2 5 z (z+5)/(z-5)^3
+4 7 z (z+7)/(z-7)^3
{{AutoCat}}
4lj9ivgf5odz5w8rxan562nztqoc5ju
773224
773223
2026-09-26T09:55:57Z
Xhungab
23827
773224
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc initiation (livre)]]
[[Mathc initiation/0075|Sommaire]]
* [[Modification de Mathc initiation/0079#Le retard de no unités : Z(x(n-no)) = z^(-no) X(z)| '''Le retard de no unités : Z(x[n-no]) = z^(-no) X(z)''']]
* [[#Multiplication par une exponentielle : a^n x(n)| '''Multiplication par une exponentielle : a^n x[n]''']]
* [[#Multiplication par la variable d'évolution : Z(n x(n)) = -z X'(z)| '''Multiplication par la variable d'évolution : Z(n x[n]) = -z X'(z)''']]
* [[#Multiplication par la variable d'évolution n^2 : Z(n^2 x(n)) = -z X"(z)| '''Multiplication par la variable d'évolution n^2 : Z(n^2 x[n]) = -z X"(z)''']]
==Le retard de no unités : Z(x(n-no)) = z^(-no) X(z)==
Donnez la transformée en z de cette équation :
y[n]
- 3 y[n-1]
+ 5 y[n-2]
- 7 y[n-3] = δ[n]
Les valeurs initiales y[-1] = 2; y[-2] = 4; y[-3] = 6;
Y(z)
- 3 (z^(-1) Y(z) + y[-1])
+ 5 (z^(-2) Y(z) + z^(-1) y[-1] + y[-2])
- 7 (z^(-3) Y(z) + z^(-2) y[-1] + z^(-1)y[-2] + y[-3]) = 1
==Multiplication par une exponentielle : a^n x(n)==
Donnez la transformée en z de cette équation :
y[n] =
- 3^(n) u[n]
+ 2 5^(n) u[n]
+ 4 7^(n) u[n]
Y(z) =
- z/(z-3)
+ 2 z/(z-5)
+ 4 z/(z-7)
==Multiplication par la variable d'évolution : Z(n x(n)) = -z X'(z)==
Donnez la transformée en z de cette équation :
y[n] =
- n 3^(n) u[n]
+ 2 n 5^(n) u[n]
+ 4 n 7^(n) u[n]
Y(z) =
- 3 z/(z-3)^2
+ 2 5 z/(z-5)^2
+ 4 7 z/(z-7)^2
==Multiplication par la variable d'évolution n^2 : Z(n^2 x(n)) = -z X"(z)==
Donnez la transformée en z de cette équation :
y[n] =
- n^2 3^(n) u[n]
+ 2 n^2 5^(n) u[n]
+ 4 n^2 7^(n) u[n]
Y(z) =
- 3 z (z+3)/(z-3)^3
+2 5 z (z+5)/(z-5)^3
+4 7 z (z+7)/(z-7)^3
{{AutoCat}}
p3a7lmu2tb3oztpmf720rfx97msmjeq
773225
773224
2026-09-26T09:56:18Z
Xhungab
23827
773225
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc initiation (livre)]]
[[Mathc initiation/0075|Sommaire]]
* [[Mathc initiation/0079#Le retard de no unités : Z(x(n-no)) = z^(-no) X(z)| '''Le retard de no unités : Z(x[n-no]) = z^(-no) X(z)''']]
* [[#Multiplication par une exponentielle : a^n x(n)| '''Multiplication par une exponentielle : a^n x[n]''']]
* [[#Multiplication par la variable d'évolution : Z(n x(n)) = -z X'(z)| '''Multiplication par la variable d'évolution : Z(n x[n]) = -z X'(z)''']]
* [[#Multiplication par la variable d'évolution n^2 : Z(n^2 x(n)) = -z X"(z)| '''Multiplication par la variable d'évolution n^2 : Z(n^2 x[n]) = -z X"(z)''']]
==Le retard de no unités : Z(x(n-no)) = z^(-no) X(z)==
Donnez la transformée en z de cette équation :
y[n]
- 3 y[n-1]
+ 5 y[n-2]
- 7 y[n-3] = δ[n]
Les valeurs initiales y[-1] = 2; y[-2] = 4; y[-3] = 6;
Y(z)
- 3 (z^(-1) Y(z) + y[-1])
+ 5 (z^(-2) Y(z) + z^(-1) y[-1] + y[-2])
- 7 (z^(-3) Y(z) + z^(-2) y[-1] + z^(-1)y[-2] + y[-3]) = 1
==Multiplication par une exponentielle : a^n x(n)==
Donnez la transformée en z de cette équation :
y[n] =
- 3^(n) u[n]
+ 2 5^(n) u[n]
+ 4 7^(n) u[n]
Y(z) =
- z/(z-3)
+ 2 z/(z-5)
+ 4 z/(z-7)
==Multiplication par la variable d'évolution : Z(n x(n)) = -z X'(z)==
Donnez la transformée en z de cette équation :
y[n] =
- n 3^(n) u[n]
+ 2 n 5^(n) u[n]
+ 4 n 7^(n) u[n]
Y(z) =
- 3 z/(z-3)^2
+ 2 5 z/(z-5)^2
+ 4 7 z/(z-7)^2
==Multiplication par la variable d'évolution n^2 : Z(n^2 x(n)) = -z X"(z)==
Donnez la transformée en z de cette équation :
y[n] =
- n^2 3^(n) u[n]
+ 2 n^2 5^(n) u[n]
+ 4 n^2 7^(n) u[n]
Y(z) =
- 3 z (z+3)/(z-3)^3
+2 5 z (z+5)/(z-5)^3
+4 7 z (z+7)/(z-7)^3
{{AutoCat}}
0jh8dg3bksh77o0g3gnxkz12unh9uda
773226
773225
2026-09-26T09:57:20Z
Xhungab
23827
773226
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc initiation (livre)]]
[[Mathc initiation/0075|Sommaire]]
* [[#Le retard de no unités| '''Le retard de no unités : Z(x[n-no]) = z^(-no) X(z)''']]
* [[#Multiplication par une exponentielle : a^n x(n)| '''Multiplication par une exponentielle : a^n x[n]''']]
* [[#Multiplication par la variable d'évolution : Z(n x(n)) = -z X'(z)| '''Multiplication par la variable d'évolution : Z(n x[n]) = -z X'(z)''']]
* [[#Multiplication par la variable d'évolution n^2 : Z(n^2 x(n)) = -z X"(z)| '''Multiplication par la variable d'évolution n^2 : Z(n^2 x[n]) = -z X"(z)''']]
==Le retard de no unités==
: Z(x(n-no)) = z^(-no) X(z)
Donnez la transformée en z de cette équation :
y[n]
- 3 y[n-1]
+ 5 y[n-2]
- 7 y[n-3] = δ[n]
Les valeurs initiales y[-1] = 2; y[-2] = 4; y[-3] = 6;
Y(z)
- 3 (z^(-1) Y(z) + y[-1])
+ 5 (z^(-2) Y(z) + z^(-1) y[-1] + y[-2])
- 7 (z^(-3) Y(z) + z^(-2) y[-1] + z^(-1)y[-2] + y[-3]) = 1
==Multiplication par une exponentielle : a^n x(n)==
Donnez la transformée en z de cette équation :
y[n] =
- 3^(n) u[n]
+ 2 5^(n) u[n]
+ 4 7^(n) u[n]
Y(z) =
- z/(z-3)
+ 2 z/(z-5)
+ 4 z/(z-7)
==Multiplication par la variable d'évolution : Z(n x(n)) = -z X'(z)==
Donnez la transformée en z de cette équation :
y[n] =
- n 3^(n) u[n]
+ 2 n 5^(n) u[n]
+ 4 n 7^(n) u[n]
Y(z) =
- 3 z/(z-3)^2
+ 2 5 z/(z-5)^2
+ 4 7 z/(z-7)^2
==Multiplication par la variable d'évolution n^2 : Z(n^2 x(n)) = -z X"(z)==
Donnez la transformée en z de cette équation :
y[n] =
- n^2 3^(n) u[n]
+ 2 n^2 5^(n) u[n]
+ 4 n^2 7^(n) u[n]
Y(z) =
- 3 z (z+3)/(z-3)^3
+2 5 z (z+5)/(z-5)^3
+4 7 z (z+7)/(z-7)^3
{{AutoCat}}
e4ayuxx67s4jjft9cshtbmijcbiafyx
773227
773226
2026-09-26T09:58:30Z
Xhungab
23827
773227
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc initiation (livre)]]
[[Mathc initiation/0075|Sommaire]]
* [[#Le retard de no unités| '''Le retard de no unités : Z(x[n-no]) = z^(-no) X(z)''']]
* [[#Multiplication par une exponentielle : a^n x(n)| '''Multiplication par une exponentielle : a^n x[n]''']]
* [[#Multiplication par la variable d'évolution : Z(n x(n)) = -z X'(z)| '''Multiplication par la variable d'évolution : Z(n x[n]) = -z X'(z)''']]
* [[#Multiplication par la variable d'évolution n^2 : Z(n^2 x(n)) = -z X"(z)| '''Multiplication par la variable d'évolution n^2 : Z(n^2 x[n]) = -z X"(z)''']]
== Le retard de no unités ==
: Z(x(n-no)) = z^(-no) X(z)
Donnez la transformée en z de cette équation :
y[n]
- 3 y[n-1]
+ 5 y[n-2]
- 7 y[n-3] = δ[n]
Les valeurs initiales y[-1] = 2; y[-2] = 4; y[-3] = 6;
Y(z)
- 3 (z^(-1) Y(z) + y[-1])
+ 5 (z^(-2) Y(z) + z^(-1) y[-1] + y[-2])
- 7 (z^(-3) Y(z) + z^(-2) y[-1] + z^(-1)y[-2] + y[-3]) = 1
==Multiplication par une exponentielle : a^n x(n)==
Donnez la transformée en z de cette équation :
y[n] =
- 3^(n) u[n]
+ 2 5^(n) u[n]
+ 4 7^(n) u[n]
Y(z) =
- z/(z-3)
+ 2 z/(z-5)
+ 4 z/(z-7)
==Multiplication par la variable d'évolution : Z(n x(n)) = -z X'(z)==
Donnez la transformée en z de cette équation :
y[n] =
- n 3^(n) u[n]
+ 2 n 5^(n) u[n]
+ 4 n 7^(n) u[n]
Y(z) =
- 3 z/(z-3)^2
+ 2 5 z/(z-5)^2
+ 4 7 z/(z-7)^2
==Multiplication par la variable d'évolution n^2 : Z(n^2 x(n)) = -z X"(z)==
Donnez la transformée en z de cette équation :
y[n] =
- n^2 3^(n) u[n]
+ 2 n^2 5^(n) u[n]
+ 4 n^2 7^(n) u[n]
Y(z) =
- 3 z (z+3)/(z-3)^3
+2 5 z (z+5)/(z-5)^3
+4 7 z (z+7)/(z-7)^3
{{AutoCat}}
3ifoglng0qx4g16rj4vnn3h7tcfetz3
773228
773227
2026-09-26T09:59:37Z
Xhungab
23827
773228
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc initiation (livre)]]
[[Mathc initiation/0075|Sommaire]]
* [[#Diagonal matrix| '''Matrice diagonale''']]
* [[#Le retard de no unités| '''Le retard de no unités : Z(x[n-no]) = z^(-no) X(z)''']]
* [[#Multiplication par une exponentielle : a^n x(n)| '''Multiplication par une exponentielle : a^n x[n]''']]
* [[#Multiplication par la variable d'évolution : Z(n x(n)) = -z X'(z)| '''Multiplication par la variable d'évolution : Z(n x[n]) = -z X'(z)''']]
* [[#Multiplication par la variable d'évolution n^2 : Z(n^2 x(n)) = -z X"(z)| '''Multiplication par la variable d'évolution n^2 : Z(n^2 x[n]) = -z X"(z)''']]
== Diagonal matrix ==
:
:
== Le retard de no unités ==
: Z(x(n-no)) = z^(-no) X(z)
Donnez la transformée en z de cette équation :
y[n]
- 3 y[n-1]
+ 5 y[n-2]
- 7 y[n-3] = δ[n]
Les valeurs initiales y[-1] = 2; y[-2] = 4; y[-3] = 6;
Y(z)
- 3 (z^(-1) Y(z) + y[-1])
+ 5 (z^(-2) Y(z) + z^(-1) y[-1] + y[-2])
- 7 (z^(-3) Y(z) + z^(-2) y[-1] + z^(-1)y[-2] + y[-3]) = 1
==Multiplication par une exponentielle : a^n x(n)==
Donnez la transformée en z de cette équation :
y[n] =
- 3^(n) u[n]
+ 2 5^(n) u[n]
+ 4 7^(n) u[n]
Y(z) =
- z/(z-3)
+ 2 z/(z-5)
+ 4 z/(z-7)
==Multiplication par la variable d'évolution : Z(n x(n)) = -z X'(z)==
Donnez la transformée en z de cette équation :
y[n] =
- n 3^(n) u[n]
+ 2 n 5^(n) u[n]
+ 4 n 7^(n) u[n]
Y(z) =
- 3 z/(z-3)^2
+ 2 5 z/(z-5)^2
+ 4 7 z/(z-7)^2
==Multiplication par la variable d'évolution n^2 : Z(n^2 x(n)) = -z X"(z)==
Donnez la transformée en z de cette équation :
y[n] =
- n^2 3^(n) u[n]
+ 2 n^2 5^(n) u[n]
+ 4 n^2 7^(n) u[n]
Y(z) =
- 3 z (z+3)/(z-3)^3
+2 5 z (z+5)/(z-5)^3
+4 7 z (z+7)/(z-7)^3
{{AutoCat}}
4h11a045bst36jhheo025ewearsocxq
773229
773228
2026-09-26T10:01:22Z
Xhungab
23827
773229
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc initiation (livre)]]
[[Mathc initiation/0075|Sommaire]]
* [[#Le retard de no unités : Z(x(n-no)) = z^(-no) X(z)| '''Le retard de no unités : Z(x[n-no]) = z^(-no) X(z)''']]
* [[#Multiplication par une exponentielle : a^n x(n)| '''Multiplication par une exponentielle : a^n x[n]''']]
* [[#Multiplication par la variable d'évolution : Z(n x(n)) = -z X'(z)| '''Multiplication par la variable d'évolution : Z(n x[n]) = -z X'(z)''']]
* [[#Multiplication par la variable d'évolution n^2 : Z(n^2 x(n)) = -z X"(z)| '''Multiplication par la variable d'évolution n^2 : Z(n^2 x[n]) = -z X"(z)''']]
== Le retard de no unités : Z(x(n-no)) = z^(-no) X(z) ==
Donnez la transformée en z de cette équation :
y[n]
- 3 y[n-1]
+ 5 y[n-2]
- 7 y[n-3] = δ[n]
Les valeurs initiales y[-1] = 2; y[-2] = 4; y[-3] = 6;
Y(z)
- 3 (z^(-1) Y(z) + y[-1])
+ 5 (z^(-2) Y(z) + z^(-1) y[-1] + y[-2])
- 7 (z^(-3) Y(z) + z^(-2) y[-1] + z^(-1)y[-2] + y[-3]) = 1
==Multiplication par une exponentielle : a^n x(n)==
Donnez la transformée en z de cette équation :
y[n] =
- 3^(n) u[n]
+ 2 5^(n) u[n]
+ 4 7^(n) u[n]
Y(z) =
- z/(z-3)
+ 2 z/(z-5)
+ 4 z/(z-7)
==Multiplication par la variable d'évolution : Z(n x(n)) = -z X'(z)==
Donnez la transformée en z de cette équation :
y[n] =
- n 3^(n) u[n]
+ 2 n 5^(n) u[n]
+ 4 n 7^(n) u[n]
Y(z) =
- 3 z/(z-3)^2
+ 2 5 z/(z-5)^2
+ 4 7 z/(z-7)^2
==Multiplication par la variable d'évolution n^2 : Z(n^2 x(n)) = -z X"(z)==
Donnez la transformée en z de cette équation :
y[n] =
- n^2 3^(n) u[n]
+ 2 n^2 5^(n) u[n]
+ 4 n^2 7^(n) u[n]
Y(z) =
- 3 z (z+3)/(z-3)^3
+2 5 z (z+5)/(z-5)^3
+4 7 z (z+7)/(z-7)^3
{{AutoCat}}
dw6gj7n777ijbdpedxomv6gpcy2701z
773230
773229
2026-09-26T10:02:13Z
Xhungab
23827
773230
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc initiation (livre)]]
[[Mathc initiation/0075|Sommaire]]
* [[#Le retard de no unités : Z(x(n-no)) = z^(-no) X(z)| '''Le retard de no unités : Z(x[n-no]) = z^(-no) X(z)''']]
* [[#Multiplication par une exponentielle : a^n x(n)| '''Multiplication par une exponentielle : a^n x[n]''']]
* [[#Multiplication par la variable d'évolution : Z(n x(n)) = -z X'(z)| '''Multiplication par la variable d'évolution : Z(n x[n]) = -z X'(z)''']]
* [[#Multiplication par la variable d'évolution n^2 : Z(n^2 x(n)) = -z X"(z)| '''Multiplication par la variable d'évolution n^2 : Z(n^2 x[n]) = -z X"(z)''']]
== Le retard de no unités : Z(x(n-no)) = z^(-no) X(z) ==
Donnez la transformée en z de cette équation :
y[n]
- 3 y[n-1]
+ 5 y[n-2]
- 7 y[n-3] = δ[n]
Les valeurs initiales y[-1] = 2; y[-2] = 4; y[-3] = 6;
Y(z)
- 3 (z^(-1) Y(z) + y[-1])
+ 5 (z^(-2) Y(z) + z^(-1) y[-1] + y[-2])
- 7 (z^(-3) Y(z) + z^(-2) y[-1] + z^(-1)y[-2] + y[-3]) = 1
==Multiplication par une exponentielle : a^n x(n)==
Donnez la transformée en z de cette équation :
y[n] =
- 3^(n) u[n]
+ 2 5^(n) u[n]
+ 4 7^(n) u[n]
Y(z) =
- z/(z-3)
+ 2 z/(z-5)
+ 4 z/(z-7)
==Multiplication par la variable d'évolution : Z(n x(n)) = -z X'(z)==
Donnez la transformée en z de cette équation :
y[n] =
- n 3^(n) u[n]
+ 2 n 5^(n) u[n]
+ 4 n 7^(n) u[n]
Y(z) =
- 3 z/(z-3)^2
+ 2 5 z/(z-5)^2
+ 4 7 z/(z-7)^2
==Multiplication par la variable d'évolution n^2 : Z(n^2 x(n)) = -z X"(z)==
Donnez la transformée en z de cette équation :
y[n] =
- n^2 3^(n) u[n]
+ 2 n^2 5^(n) u[n]
+ 4 n^2 7^(n) u[n]
Y(z) =
- 3 z (z+3)/(z-3)^3
+2 5 z (z+5)/(z-5)^3
+4 7 z (z+7)/(z-7)^3
{{AutoCat}}
6el8bdi0k6ukpvmmag86d8e9nzmj2p8
773231
773230
2026-09-26T10:04:48Z
Xhungab
23827
773231
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc initiation (livre)]]
[[Mathc initiation/a522#Analyse IV : Se familiariser avec la transformée en Z|Sommaire]]
* [[#Le retard de no unités : Z(x(n-no)) = z^(-no) X(z)| '''Le retard de no unités : Z(x[n-no]) = z^(-no) X(z)''']]
* [[#Multiplication par une exponentielle : a^n x(n)| '''Multiplication par une exponentielle : a^n x[n]''']]
* [[#Multiplication par la variable d'évolution : Z(n x(n)) = -z X'(z)| '''Multiplication par la variable d'évolution : Z(n x[n]) = -z X'(z)''']]
* [[#Multiplication par la variable d'évolution n^2 : Z(n^2 x(n)) = -z X"(z)| '''Multiplication par la variable d'évolution n^2 : Z(n^2 x[n]) = -z X"(z)''']]
== Le retard de no unités : Z(x(n-no)) = z^(-no) X(z) ==
Donnez la transformée en z de cette équation :
y[n]
- 3 y[n-1]
+ 5 y[n-2]
- 7 y[n-3] = δ[n]
Les valeurs initiales y[-1] = 2; y[-2] = 4; y[-3] = 6;
Y(z)
- 3 (z^(-1) Y(z) + y[-1])
+ 5 (z^(-2) Y(z) + z^(-1) y[-1] + y[-2])
- 7 (z^(-3) Y(z) + z^(-2) y[-1] + z^(-1)y[-2] + y[-3]) = 1
==Multiplication par une exponentielle : a^n x(n)==
Donnez la transformée en z de cette équation :
y[n] =
- 3^(n) u[n]
+ 2 5^(n) u[n]
+ 4 7^(n) u[n]
Y(z) =
- z/(z-3)
+ 2 z/(z-5)
+ 4 z/(z-7)
==Multiplication par la variable d'évolution : Z(n x(n)) = -z X'(z)==
Donnez la transformée en z de cette équation :
y[n] =
- n 3^(n) u[n]
+ 2 n 5^(n) u[n]
+ 4 n 7^(n) u[n]
Y(z) =
- 3 z/(z-3)^2
+ 2 5 z/(z-5)^2
+ 4 7 z/(z-7)^2
==Multiplication par la variable d'évolution n^2 : Z(n^2 x(n)) = -z X"(z)==
Donnez la transformée en z de cette équation :
y[n] =
- n^2 3^(n) u[n]
+ 2 n^2 5^(n) u[n]
+ 4 n^2 7^(n) u[n]
Y(z) =
- 3 z (z+3)/(z-3)^3
+2 5 z (z+5)/(z-5)^3
+4 7 z (z+7)/(z-7)^3
{{AutoCat}}
dq15zoeawqg9jeujfwp8sadjsod1d7e
773232
773231
2026-09-26T10:25:34Z
Xhungab
23827
773232
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc initiation (livre)]]
[[Mathc initiation/a522#Analyse IV : Se familiariser avec la transformée en Z|Sommaire]]
* [[#Le retard de no unités : Z(x(n-no)) = z^(-no) X(z)| '''Le retard de no unités : Z(x[n-no]) = z^(-no) X(z)''']]
* [[#Multiplication par une exponentielle : a^n x(n)| '''Multiplication par une exponentielle : a^n x[n]''']]
* [[#Multiplication par la variable d'évolution : Z(n x(n)) = -z X'(z)| '''Multiplication par la variable d'évolution : Z(n x[n]) = -z X'(z)''']]
* [[#Multiplication par la variable d'évolution n^2 : Z(n^2 x(n)) = -z X"(z)| '''Multiplication par la variable d'évolution n^2 : Z(n^2 x[n]) = -z X"(z)''']]
Remarque: Voir la page [[Mathc initiation/a583|la Transformée en Z : Quelques propriétés]]
== Le retard de no unités : Z(x(n-no)) = z^(-no) X(z) ==
Donnez la transformée en z de cette équation :
y[n]
- 3 y[n-1]
+ 5 y[n-2]
- 7 y[n-3] = δ[n]
Les valeurs initiales y[-1] = 2; y[-2] = 4; y[-3] = 6;
Y(z)
- 3 (z^(-1) Y(z) + y[-1])
+ 5 (z^(-2) Y(z) + z^(-1) y[-1] + y[-2])
- 7 (z^(-3) Y(z) + z^(-2) y[-1] + z^(-1)y[-2] + y[-3]) = 1
==Multiplication par une exponentielle : a^n x(n)==
Donnez la transformée en z de cette équation :
y[n] =
- 3^(n) u[n]
+ 2 5^(n) u[n]
+ 4 7^(n) u[n]
Y(z) =
- z/(z-3)
+ 2 z/(z-5)
+ 4 z/(z-7)
==Multiplication par la variable d'évolution : Z(n x(n)) = -z X'(z)==
Donnez la transformée en z de cette équation :
y[n] =
- n 3^(n) u[n]
+ 2 n 5^(n) u[n]
+ 4 n 7^(n) u[n]
Y(z) =
- 3 z/(z-3)^2
+ 2 5 z/(z-5)^2
+ 4 7 z/(z-7)^2
==Multiplication par la variable d'évolution n^2 : Z(n^2 x(n)) = -z X"(z)==
Donnez la transformée en z de cette équation :
y[n] =
- n^2 3^(n) u[n]
+ 2 n^2 5^(n) u[n]
+ 4 n^2 7^(n) u[n]
Y(z) =
- 3 z (z+3)/(z-3)^3
+2 5 z (z+5)/(z-5)^3
+4 7 z (z+7)/(z-7)^3
{{AutoCat}}
r4ej8h4nccpt4g2qlxp3efywbtb19gq
773233
773232
2026-09-26T10:26:57Z
Xhungab
23827
773233
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc initiation (livre)]]
[[Mathc initiation/a522#Analyse IV : Se familiariser avec la transformée en Z|Sommaire]]
* [[#Le retard de no unités : Z(x(n-no)) = z^(-no) X(z)| '''Le retard de no unités : Z(x[n-no]) = z^(-no) X(z)''']]
* [[#Multiplication par une exponentielle : a^n x(n)| '''Multiplication par une exponentielle : a^n x[n]''']]
* [[#Multiplication par la variable d'évolution : Z(n x(n)) = -z X'(z)| '''Multiplication par la variable d'évolution : Z(n x[n]) = -z X'(z)''']]
* [[#Multiplication par la variable d'évolution n^2 : Z(n^2 x(n)) = -z X"(z)| '''Multiplication par la variable d'évolution n^2 : Z(n^2 x[n]) = -z X"(z)''']]
Remarque: Voir la page [[Mathc initiation/a583|Transformée en Z : Quelques propriétés]]
== Le retard de no unités : Z(x(n-no)) = z^(-no) X(z) ==
Donnez la transformée en z de cette équation :
y[n]
- 3 y[n-1]
+ 5 y[n-2]
- 7 y[n-3] = δ[n]
Les valeurs initiales y[-1] = 2; y[-2] = 4; y[-3] = 6;
Y(z)
- 3 (z^(-1) Y(z) + y[-1])
+ 5 (z^(-2) Y(z) + z^(-1) y[-1] + y[-2])
- 7 (z^(-3) Y(z) + z^(-2) y[-1] + z^(-1)y[-2] + y[-3]) = 1
==Multiplication par une exponentielle : a^n x(n)==
Donnez la transformée en z de cette équation :
y[n] =
- 3^(n) u[n]
+ 2 5^(n) u[n]
+ 4 7^(n) u[n]
Y(z) =
- z/(z-3)
+ 2 z/(z-5)
+ 4 z/(z-7)
==Multiplication par la variable d'évolution : Z(n x(n)) = -z X'(z)==
Donnez la transformée en z de cette équation :
y[n] =
- n 3^(n) u[n]
+ 2 n 5^(n) u[n]
+ 4 n 7^(n) u[n]
Y(z) =
- 3 z/(z-3)^2
+ 2 5 z/(z-5)^2
+ 4 7 z/(z-7)^2
==Multiplication par la variable d'évolution n^2 : Z(n^2 x(n)) = -z X"(z)==
Donnez la transformée en z de cette équation :
y[n] =
- n^2 3^(n) u[n]
+ 2 n^2 5^(n) u[n]
+ 4 n^2 7^(n) u[n]
Y(z) =
- 3 z (z+3)/(z-3)^3
+2 5 z (z+5)/(z-5)^3
+4 7 z (z+7)/(z-7)^3
{{AutoCat}}
npc0szfk96j4i5o18ckjinhvw9oeukm